// The MIT License
//
// Copyright (c) 2015 Gwendal Roué
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.


import Foundation

/// See the documentation of the `TemplateRepositoryDataSource` protocol.
public typealias TemplateID = String

/// The protocol for a TemplateRepository's dataSource.
/// 
/// The dataSource's responsability is to provide Mustache template strings for
/// template and partial names.
public protocol TemplateRepositoryDataSource {
    
    /// Returns a template ID, that is to say a string that uniquely identifies
    /// a template or a template partial.
    /// 
    /// The meaning of this String is opaque: your implementation of a
    /// `TemplateRepositoryDataSource` would define, for itself, how strings
    /// would identity a template or a partial. For example, a file-based data
    /// source may use paths to the templates.
    /// 
    /// The return value of this method can be nil: the library user will then
    /// eventually catch a missing template error.
    /// 
    /// ### Template hierarchies
    /// 
    /// Whenever relevant, template and partial hierarchies are supported via
    /// the baseTemplateID parameter: it contains the template ID of the
    /// enclosing template, or nil when the data source is asked for a
    /// template ID from a raw template string (see
    /// TemplateRepository.template(string:error:)).
    /// 
    /// Not all data sources have to implement hierarchies: they can simply
    /// ignore this parameter.
    /// 
    /// *Well-behaved* data sources that support hierarchies should support
    /// "absolute paths to partials". For example, the built-in directory-based
    /// data source lets users write both `{{> relative/path/to/partial}}` and
    /// `{{> /absolute/path/tp/partial}}`tags.
    /// 
    /// ### Unique IDs
    /// 
    /// Each template must be identified by a single template ID. For example, a
    /// file-based data source which uses path IDs must normalize paths. Not
    /// following this rule yields undefined behavior.
    /// 
    /// - parameter name: The name of the template or template partial.
    /// - parameter baseTemplateID: The template ID of the enclosing template.
    /// - returns: A template ID.
    func templateIDForName(_ name: String, relativeToTemplateID baseTemplateID: TemplateID?) -> TemplateID?
    
    /// Returns the Mustache template string that matches the template ID.
    /// 
    /// - parameter templateID: The template ID of the template.
    /// - throws: MustacheError
    /// - returns: A Mustache template string.
    func templateStringForTemplateID(_ templateID: TemplateID) throws -> String
}

/// A template repository represents a set of sibling templates and partials.
/// 
/// You don't have to instanciate template repositories, because GRMustache
/// provides implicit ones whenever you load templates with methods like
/// `Template(named:error:)`, for example.
/// 
/// However, you may like to use one for your profit. Template repositories
/// provide:
/// 
/// - custom template data source
/// - custom `Configuration`
/// - a cache of template parsings
/// - absolute paths to ease loading of partial templates in a hierarchy of
///   directories and template files
final public class TemplateRepository {
    
    // =========================================================================
    // MARK: - Creating Template Repositories
    
    /// Creates a TemplateRepository which loads template through the provided
    /// dataSource.
    /// 
    /// The dataSource is optional, but repositories without dataSource can not
    /// load templates by name, and can only parse template strings that do not
    /// contain any `{{> partial }}` tag.
    /// 
    ///     let repository = TemplateRepository()
    ///     let template = try! repository.template(string: "Hello {{name}}")
    public init(dataSource: TemplateRepositoryDataSource? = nil, configuration: Configuration = .default) {
        self.configuration = configuration
        templateASTCache = [:]
        self.dataSource = dataSource
    }
    
    /// Creates a TemplateRepository that loads templates from a dictionary.
    /// 
    ///     let templates = ["template": "Hulk Hogan has a Mustache."]
    ///     let repository = TemplateRepository(templates: templates)
    /// 
    ///     // Renders "Hulk Hogan has a Mustache." twice
    ///     try! repository.template(named: "template").render()
    ///     try! repository.template(string: "{{>template}}").render()
    /// 
    /// - parameter templates: A dictionary whose keys are template names and
    ///   values template strings.
    /// - parameter configuration: The configuration for rendering. If the configuration is not specified, `Configuration.default` is used.
    convenience public init(templates: [String: String], configuration: Configuration = .default) {
        self.init(dataSource: DictionaryDataSource(templates: templates), configuration: configuration)
    }
    
    /// Creates a TemplateRepository that loads templates from a directory.
    /// 
    ///     let repository = TemplateRepository(directoryPath: "/path/to/templates")
    /// 
    ///     // Loads /path/to/templates/template.mustache
    ///     let template = try! repository.template(named: "template")
    /// 
    /// 
    /// Eventual partial tags in template files refer to sibling template files.
    /// 
    /// If the target directory contains a hierarchy of template files and
    /// sub-directories, you can navigate through this hierarchy with both
    /// relative and absolute paths to partials. For example, given the
    /// following hierarchy:
    /// 
    /// - /path/to/templates
    ///     - a.mustache
    ///     - partials
    ///         - b.mustache
    /// 
    /// The a.mustache template can embed b.mustache with both
    /// `{{> partials/b }}` and `{{> /partials/b }}` partial tags.
    /// 
    /// The b.mustache template can embed a.mustache with both `{{> ../a }}` and
    /// `{{> /a }}` partial tags.
    /// 
    /// 
    /// - parameter directoryPath: The path to the directory containing
    ///   template files.
    /// - parameter templateExtension: The extension of template files. Default
    ///   extension is "mustache".
    /// - parameter encoding: The encoding of template files. Default encoding
    ///   is UTF-8.
    /// - parameter configuration: The configuration for rendering. If the configuration is not specified, `Configuration.default` is used.
    convenience public init(directoryPath: String, templateExtension: String? = "mustache", encoding: String.Encoding = String.Encoding.utf8, configuration: Configuration = .default) {
        self.init(dataSource: URLDataSource(baseURL: URL(fileURLWithPath: directoryPath, isDirectory: true), templateExtension: templateExtension, encoding: encoding), configuration: configuration)
    }
    
    /// Creates a TemplateRepository that loads templates from a URL.
    /// 
    ///     let templatesURL = NSURL.fileURLWithPath("/path/to/templates")!
    ///     let repository = TemplateRepository(baseURL: templatesURL)
    /// 
    ///     // Loads /path/to/templates/template.mustache
    ///     let template = try! repository.template(named: "template")
    /// 
    /// 
    /// Eventual partial tags in template files refer to sibling template files.
    /// 
    /// If the target directory contains a hierarchy of template files and
    /// sub-directories, you can navigate through this hierarchy with both
    /// relative and absolute paths to partials. For example, given the
    /// following hierarchy:
    /// 
    /// - /path/to/templates
    ///     - a.mustache
    ///     - partials
    ///         - b.mustache
    /// 
    /// The a.mustache template can embed b.mustache with both `{{> partials/b }}`
    /// and `{{> /partials/b }}` partial tags.
    /// 
    /// The b.mustache template can embed a.mustache with both `{{> ../a }}` and
    /// `{{> /a }}` partial tags.
    /// 
    /// 
    /// - parameter baseURL: The base URL where to look for templates.
    /// - parameter templateExtension: The extension of template resources.
    ///   Default extension is "mustache".
    /// - parameter encoding: The encoding of template resources. Default
    ///   encoding is UTF-8.
    /// - parameter configuration: The configuration for rendering. If the configuration is not specified, `Configuration.default` is used.
    convenience public init(baseURL: URL, templateExtension: String? = "mustache", encoding: String.Encoding = String.Encoding.utf8, configuration: Configuration = .default) {
        self.init(dataSource: URLDataSource(baseURL: baseURL, templateExtension: templateExtension, encoding: encoding), configuration: configuration)
    }
    
    /// Creates a TemplateRepository that loads templates stored as resources in
    /// a bundle.
    /// 
    ///     let repository = TemplateRepository(bundle: nil)
    /// 
    ///     // Loads the template.mustache resource of the main bundle:
    ///     let template = try! repository.template(named: "template")
    /// 
    /// - parameter bundle: The bundle that stores templates resources. Nil
    ///   stands for the main bundle.
    /// - parameter templateExtension: The extension of template resources.
    ///   Default extension is "mustache".
    /// - parameter encoding: The encoding of template resources. Default
    ///   encoding is UTF-8.
    /// - parameter configuration: The configuration for rendering. If the configuration is not specified, `Configuration.default` is used.
    convenience public init(bundle: Bundle?, templateExtension: String? = "mustache", encoding: String.Encoding = String.Encoding.utf8, configuration: Configuration = .default) {
        self.init(dataSource: BundleDataSource(bundle: bundle ?? Bundle.main, templateExtension: templateExtension, encoding: encoding), configuration: configuration)
    }
    
    
    // =========================================================================
    // MARK: - Configuring Template Repositories
    
    /// The configuration for all templates and partials built by
    /// the repository.
    /// 
    /// It is initialized with `Mustache.DefaultConfiguration`.
    /// 
    /// You can alter the repository's configuration, or set it to another
    /// value, before you load templates:
    /// 
    ///     // Reset the configuration to a factory configuration and change tag delimiters:
    ///     let repository = TemplateRepository()
    ///     repository.configuration = Configuration()
    ///     repository.configuration.tagDelimiterPair = ("<%", "%>")
    /// 
    ///     // Renders "Hello Luigi"
    ///     let template = try! repository.template(string: "Hello <%name%>")
    ///     try! template.render(["name": "Luigi"])
    /// 
    /// Changing the configuration has no effect after the repository has loaded
    /// one template.
    public var configuration: Configuration
    
    
    // =========================================================================
    // MARK: - Loading Templates from a Repository
    
    
    /// The template repository data source, responsible for loading template
    /// strings.
    public let dataSource: TemplateRepositoryDataSource?
    
    /// Creates a template from a template string.
    /// 
    /// Depending on the way the repository has been created, partial tags such
    /// as `{{>partial}}` load partial templates from URLs, file paths, keys in
    /// a dictionary, or whatever is relevant to the repository's data source.
    /// 
    /// - parameter templateString: A Mustache template string.
    /// - throws: MustacheError
    /// - returns: A Mustache Template.
    public func template(string: String) throws -> Template {
        let templateAST = try self.templateAST(string: string)
        return Template(repository: self, templateAST: templateAST, baseContext: lockedConfiguration.baseContext)
    }
    
    /// Creates a template, identified by its name.
    /// 
    /// Depending on the repository's data source, the name identifies a bundle
    /// resource, a URL, a file path, a key in a dictionary, etc.
    /// 
    /// Template repositories cache the parsing of their templates. However this
    /// method always return new Template instances, which you can further
    /// configure independently.
    /// 
    /// - parameter name: The template name.
    /// - throws: MustacheError
    /// - returns: A Mustache Template.
    /// 
    /// - seealso: reloadTemplates()
    public func template(named name: String) throws -> Template {
        let templateAST = try self.templateAST(named: name, relativeToTemplateID: nil)
        return Template(repository: self, templateAST: templateAST, baseContext: lockedConfiguration.baseContext)
    }
    
    /// Clears the cache of parsed template strings.
    /// 
    ///     // May reuse a cached parsing:
    ///     let template = try! repository.template(named:"profile")
    /// 
    ///     // Forces the reloading of the template:
    ///     repository.reloadTemplates();
    ///     let template = try! repository.template(named:"profile")
    /// 
    /// Note that previously created Template instances are not reloaded.
    public func reloadTemplates() {
        templateASTCache.removeAll()
    }
    
    
    // =========================================================================
    // MARK: - Not public
    
    func templateAST(named name: String, relativeToTemplateID baseTemplateID: TemplateID? = nil) throws -> TemplateAST {
        guard let dataSource = self.dataSource else {
            throw MustacheError(kind: .templateNotFound, message: "Missing dataSource", templateID: baseTemplateID)
        }
        
        guard let templateID = dataSource.templateIDForName(name, relativeToTemplateID: baseTemplateID) else {
            if let baseTemplateID = baseTemplateID {
                throw MustacheError(kind: .templateNotFound, message: "Template not found: \"\(name)\" from \(baseTemplateID)", templateID: baseTemplateID)
            } else {
                throw MustacheError(kind: .templateNotFound, message: "Template not found: \"\(name)\"")
            }
        }
        
        if let templateAST = templateASTCache[templateID] {
            // Return cached AST
            return templateAST
        }
        
        let templateString = try dataSource.templateStringForTemplateID(templateID)
        
        // Cache an empty AST for that name so that we support recursive
        // partials.
        let templateAST = TemplateAST()
        templateASTCache[templateID] = templateAST
        
        do {
            let compiledAST = try self.templateAST(string: templateString, templateID: templateID)
            // Success: update the empty AST
            templateAST.updateFromTemplateAST(compiledAST)
            return templateAST
        } catch {
            // Failure: remove the empty AST
            templateASTCache.removeValue(forKey: templateID)
            throw error
        }
    }

    @available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
    func templateAST(named name: String, relativeToTemplateID baseTemplateID: TemplateID? = nil) async throws -> TemplateAST {
        guard let dataSource = self.dataSource else {
            throw MustacheError(kind: .templateNotFound, message: "Missing dataSource", templateID: baseTemplateID)
        }

        guard let templateID = dataSource.templateIDForName(name, relativeToTemplateID: baseTemplateID) else {
            if let baseTemplateID = baseTemplateID {
                throw MustacheError(kind: .templateNotFound, message: "Template not found: \"\(name)\" from \(baseTemplateID)", templateID: baseTemplateID)
            } else {
                throw MustacheError(kind: .templateNotFound, message: "Template not found: \"\(name)\"")
            }
        }

        if let templateAST = templateASTCache[templateID] {
            // Return cached AST
            return templateAST
        }

        let templateString = try await dataSource.templateStringForTemplateID(templateID)

        // Cache an empty AST for that name so that we support recursive
        // partials.
        let templateAST = TemplateAST()
        templateASTCache[templateID] = templateAST

        do {
            let compiledAST = try self.templateAST(string: templateString, templateID: templateID)
            // Success: update the empty AST
            templateAST.updateFromTemplateAST(compiledAST)
            return templateAST
        } catch {
            // Failure: remove the empty AST
            templateASTCache.removeValue(forKey: templateID)
            throw error
        }
    }

    func templateAST(string: String, templateID: TemplateID? = nil) throws -> TemplateAST {
        // A Compiler
        let compiler = TemplateCompiler(
            contentType: lockedConfiguration.contentType,
            repository: self,
            templateID: templateID)
        
        // A Parser that feeds the compiler
        let parser = TemplateParser(
            tokenConsumer: compiler,
            tagDelimiterPair: lockedConfiguration.tagDelimiterPair)
        
        // Parse...
        parser.parse(string, templateID: templateID)
        
        // ...and extract the result from the Compiler
        return try compiler.templateAST()
    }
    
    
    fileprivate var _lockedConfiguration: Configuration?
    fileprivate var lockedConfiguration: Configuration {
        // Changing mutable values within the repository's configuration no
        // longer has any effect.
        if _lockedConfiguration == nil {
            _lockedConfiguration = configuration
        }
        return _lockedConfiguration!
    }
    
    fileprivate var templateASTCache: [TemplateID: TemplateAST]
    
    
    // -------------------------------------------------------------------------
    // MARK: DictionaryDataSource
    
    fileprivate class DictionaryDataSource: TemplateRepositoryDataSource {
        let templates: [String: String]
        
        init(templates: [String: String]) {
            self.templates = templates
        }
        
        func templateIDForName(_ name: String, relativeToTemplateID baseTemplateID: TemplateID?) -> TemplateID? {
            return name
        }
        
        func templateStringForTemplateID(_ templateID: TemplateID) throws -> String {
            if let string = templates[templateID] {
                return string
            } else {
                throw MustacheError(kind: .templateNotFound, templateID: templateID)
            }
        }
    }
    
    
    // -------------------------------------------------------------------------
    // MARK: URLDataSource
    
    fileprivate class URLDataSource: TemplateRepositoryDataSource {
        let baseURLAbsoluteString: String
        let baseURL: URL
        let templateExtension: String?
        let encoding: String.Encoding
        
        init(baseURL: URL, templateExtension: String?, encoding: String.Encoding) {
            self.baseURL = baseURL
            self.baseURLAbsoluteString = baseURL.standardizedFileURL.absoluteString
            self.templateExtension = templateExtension
            self.encoding = encoding
        }
        
        func templateIDForName(_ name: String, relativeToTemplateID baseTemplateID: TemplateID?) -> TemplateID? {
            // Rebase template names starting with a /
            let normalizedName: String
            let normalizedBaseTemplateID: TemplateID?
            if !name.isEmpty && name[name.startIndex] == "/" {
                normalizedName = String(name[name.index(after: name.startIndex)...])
                normalizedBaseTemplateID = nil
            } else {
                normalizedName = name
                normalizedBaseTemplateID = baseTemplateID
            }
        
            if normalizedName.isEmpty {
                return normalizedBaseTemplateID
            }
            
            let templateFilename: String
            if let templateExtension = self.templateExtension , !templateExtension.isEmpty {
                templateFilename = (normalizedName as NSString).appendingPathExtension(templateExtension)!
            } else {
                templateFilename = normalizedName
            }
            
            let templateBaseURL: URL
            if let normalizedBaseTemplateID = normalizedBaseTemplateID {
                templateBaseURL = URL(string: normalizedBaseTemplateID)!
            } else {
                templateBaseURL = self.baseURL
            }

            let encodedURLPath = templateFilename.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
            let templateURL = URL(string: encodedURLPath, relativeTo: templateBaseURL)!.standardizedFileURL
            let templateAbsoluteString = templateURL.absoluteString
            
            // Make sure partial relative paths can not escape repository root
            if templateAbsoluteString.range(of: baseURLAbsoluteString)?.lowerBound == templateAbsoluteString.startIndex {
                return templateAbsoluteString
            } else {
                return nil
            }
        }
        
        func templateStringForTemplateID(_ templateID: TemplateID) throws -> String {
            return try NSString(contentsOf: URL(string: templateID)!, encoding: encoding.rawValue) as String
        }

        @available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
        func templateStringForTemplateID(_ templateID: TemplateID) async throws -> String {
            let (data, _) = try await URLSession.shared.data(from: URL(string: templateID)!)

            return (NSString(data: data, encoding: encoding.rawValue) ?? "") as String
        }
    }
    
    
    // -------------------------------------------------------------------------
    // MARK: BundleDataSource
    
    fileprivate class BundleDataSource: TemplateRepositoryDataSource {
        let bundle: Bundle
        let templateExtension: String?
        let encoding: String.Encoding
        
        init(bundle: Bundle, templateExtension: String?, encoding: String.Encoding) {
            self.bundle = bundle
            self.templateExtension = templateExtension
            self.encoding = encoding
        }
        
        func templateIDForName(_ name: String, relativeToTemplateID baseTemplateID: TemplateID?) -> TemplateID? {
            // Rebase template names starting with a /
            let normalizedName: String
            let normalizedBaseTemplateID: TemplateID?
            if !name.isEmpty && name[name.startIndex] == "/" {
                normalizedName = String(name[name.index(after: name.startIndex)...])
                normalizedBaseTemplateID = nil
            } else {
                normalizedName = name
                normalizedBaseTemplateID = baseTemplateID
            }
            
            if normalizedName.isEmpty {
                return normalizedBaseTemplateID
            }
            
            if let normalizedBaseTemplateID = normalizedBaseTemplateID {
                let relativePath = (normalizedBaseTemplateID as NSString).deletingLastPathComponent.replacingOccurrences(of: bundle.resourcePath!, with:"")
                return bundle.path(forResource: normalizedName, ofType: templateExtension, inDirectory: relativePath)
            } else {
                return bundle.path(forResource: normalizedName, ofType: templateExtension)
            }
        }
        
        func templateStringForTemplateID(_ templateID: TemplateID) throws -> String {
            return try NSString(contentsOfFile: templateID, encoding: encoding.rawValue) as String
        }
    }
}