//
// SQLite.swift
// https://github.com/stephencelis/SQLite.swift
// Copyright © 2014-2015 Stephen Celis.
//
// 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.
//

#if SWIFT_PACKAGE
import SQLiteObjc
#endif

extension SQLModule {

    public static func FTS4(_ column: SQLExpressible, _ more: SQLExpressible...) -> SQLModule {
        return SQLFTS4([column] + more)
    }

    public static func SQLFTS4(_ columns: [SQLExpressible] = [], tokenize tokenizer: Tokenizer? = nil) -> SQLModule {
        return FTS4(FTS4Config().columns(columns).tokenizer(tokenizer))
    }

    public static func FTS4(_ config: FTS4Config) -> SQLModule {
        return SQLModule(name: "fts4", arguments: config.arguments())
    }
}

extension VirtualTable {

    /// Builds an expression appended with a `MATCH` query against the given
    /// pattern.
    ///
    ///     let emails = VirtualTable("emails")
    ///
    ///     emails.filter(emails.match("Hello"))
    ///     // SELECT * FROM "emails" WHERE "emails" MATCH 'Hello'
    ///
    /// - Parameter pattern: A pattern to match.
    ///
    /// - Returns: An expression appended with a `MATCH` query against the given
    ///   pattern.
    public func match(_ pattern: String) -> SQLExpression<Bool> {
        return "MATCH".infix(tableName(), pattern)
    }

    public func match(_ pattern: SQLExpression<String>) -> SQLExpression<Bool> {
        return "MATCH".infix(tableName(), pattern)
    }

    public func match(_ pattern: SQLExpression<String?>) -> SQLExpression<Bool?> {
        return "MATCH".infix(tableName(), pattern)
    }

    /// Builds a copy of the query with a `WHERE … MATCH` clause.
    ///
    ///     let emails = VirtualTable("emails")
    ///
    ///     emails.match("Hello")
    ///     // SELECT * FROM "emails" WHERE "emails" MATCH 'Hello'
    ///
    /// - Parameter pattern: A pattern to match.
    ///
    /// - Returns: A query with the given `WHERE … MATCH` clause applied.
    public func match(_ pattern: String) -> SQLQueryType {
        return filter(match(pattern))
    }

    public func match(_ pattern: SQLExpression<String>) -> SQLQueryType {
        return filter(match(pattern))
    }

    public func match(_ pattern: SQLExpression<String?>) -> SQLQueryType {
        return filter(match(pattern))
    }

}

public struct Tokenizer {

    public static let Simple = Tokenizer("simple")

    public static let Porter = Tokenizer("porter")

    public static func Unicode61(removeDiacritics: Bool? = nil, tokenchars: Set<Character> = [], separators: Set<Character> = []) -> Tokenizer {
        var arguments = [String]()

        if let removeDiacritics = removeDiacritics {
            arguments.append("removeDiacritics=\(removeDiacritics ? 1 : 0)".quote())
        }

        if !tokenchars.isEmpty {
            let joined = tokenchars.map { String($0) }.joined(separator: "")
            arguments.append("tokenchars=\(joined)".quote())
        }

        if !separators.isEmpty {
            let joined = separators.map { String($0) }.joined(separator: "")
            arguments.append("separators=\(joined)".quote())
        }

        return Tokenizer("unicode61", arguments)
    }

    public static func Custom(_ name: String) -> Tokenizer {
        return Tokenizer(Tokenizer.moduleName.quote(), [name.quote()])
    }

    public let name: String

    public let arguments: [String]

    fileprivate init(_ name: String, _ arguments: [String] = []) {
        self.name = name
        self.arguments = arguments
    }

    fileprivate static let moduleName = "SQLite.swift"

}

extension Tokenizer : CustomStringConvertible {

    public var description: String {
        return ([name] + arguments).joined(separator: " ")
    }

}

extension SQLConnection {

    public func registerTokenizer(_ submoduleName: String, next: @escaping (String) -> (String, Range<String.Index>)?) throws {
        try check(_SQLiteRegisterTokenizer(handle, Tokenizer.moduleName, submoduleName) { (
                input: UnsafePointer<Int8>, offset: UnsafeMutablePointer<Int32>, length: UnsafeMutablePointer<Int32>) in
            let string = String(cString: input)

            guard let (token, range) = next(string) else { return nil }

            let view:String.UTF8View = string.utf8

            if let from = range.lowerBound.samePosition(in: view),
               let to = range.upperBound.samePosition(in: view) {
                offset.pointee += Int32(string[string.startIndex..<range.lowerBound].utf8.count)
                length.pointee = Int32(view.distance(from: from, to: to))
                return token
            } else {
                return nil
            }
        })
    }
}

/// Configuration options shared between the [FTS4](https://www.sqlite.org/fts3.html) and
/// [FTS5](https://www.sqlite.org/fts5.html) extensions.
open class SQLFTSConfig {
    public enum ColumnOption {
        /// [The notindexed= option](https://www.sqlite.org/fts3.html#section_6_5)
        case unindexed
    }

    typealias ColumnDefinition = (SQLExpressible, options: [ColumnOption])
    var columnDefinitions = [ColumnDefinition]()
    var tokenizer: Tokenizer?
    var prefixes = [Int]()
    var externalContentSchema: SQLSchemaType?
    var isContentless: Bool = false

    /// Adds a column definition
    @discardableResult open func column(_ column: SQLExpressible, _ options: [ColumnOption] = []) -> Self {
        self.columnDefinitions.append((column, options))
        return self
    }

    @discardableResult open func columns(_ columns: [SQLExpressible]) -> Self {
        for column in columns {
            self.column(column)
        }
        return self
    }

    /// [Tokenizers](https://www.sqlite.org/fts3.html#tokenizer)
    open func tokenizer(_ tokenizer: Tokenizer?) -> Self {
        self.tokenizer = tokenizer
        return self
    }

    /// [The prefix= option](https://www.sqlite.org/fts3.html#section_6_6)
    open func prefix(_ prefix: [Int]) -> Self {
        self.prefixes += prefix
        return self
    }

    /// [The content= option](https://www.sqlite.org/fts3.html#section_6_2)
    open func externalContent(_ schema: SQLSchemaType) -> Self {
        self.externalContentSchema = schema
        return self
    }

    /// [Contentless FTS4 Tables](https://www.sqlite.org/fts3.html#section_6_2_1)
    open func contentless() -> Self {
        self.isContentless = true
        return self
    }

    func formatColumnDefinitions() -> [SQLExpressible] {
        return columnDefinitions.map { $0.0 }
    }

    func arguments() -> [SQLExpressible] {
        return options().arguments
    }

    func options() -> Options {
        var options = Options()
        options.append(formatColumnDefinitions())
        if let tokenizer = tokenizer {
            options.append("tokenize", value: SQLExpression<Void>(literal: tokenizer.description))
        }
        options.appendCommaSeparated("prefix", values:prefixes.sorted().map { String($0) })
        if isContentless {
            options.append("content", value: "")
        } else if let externalContentSchema = externalContentSchema {
            options.append("content", value: externalContentSchema.tableName())
        }
        return options
    }

    struct Options {
        var arguments = [SQLExpressible]()

        @discardableResult mutating func append(_ columns: [SQLExpressible]) -> Options {
            arguments.append(contentsOf: columns)
            return self
        }

        @discardableResult mutating func appendCommaSeparated(_ key: String, values: [String]) -> Options {
            if values.isEmpty {
                return self
            } else {
                return append(key, value: values.joined(separator: ","))
            }
        }

        @discardableResult mutating func append(_ key: String, value: CustomStringConvertible?) -> Options {
            return append(key, value: value?.description)
        }

        @discardableResult mutating func append(_ key: String, value: String?) -> Options {
            return append(key, value: value.map { SQLExpression<String>($0) })
        }

        @discardableResult mutating func append(_ key: String, value: SQLExpressible?) -> Options {
            if let value = value {
                arguments.append("=".join([SQLExpression<Void>(literal: key), value]))
            }
            return self
        }
    }
}

/// Configuration for the [FTS4](https://www.sqlite.org/fts3.html) extension.
open class FTS4Config : SQLFTSConfig {
    /// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4)
    public enum MatchInfo : CustomStringConvertible {
        case fts3
        public var description: String {
            return "fts3"
        }
    }

    /// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options)
    public enum Order : CustomStringConvertible {
        /// Data structures are optimized for returning results in ascending order by docid (default)
        case asc
        /// FTS4 stores its data in such a way as to optimize returning results in descending order by docid.
        case desc

        public var description: String {
            switch self {
            case .asc: return "asc"
            case .desc: return "desc"
            }
        }
    }

    var compressFunction: String?
    var uncompressFunction: String?
    var languageId: String?
    var matchInfo: MatchInfo?
    var order: Order?

    override public init() {
    }

    /// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1)
    open func compress(_ functionName: String) -> Self {
        self.compressFunction = functionName
        return self
    }

    /// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1)
    open func uncompress(_ functionName: String) -> Self {
        self.uncompressFunction = functionName
        return self
    }

    /// [The languageid= option](https://www.sqlite.org/fts3.html#section_6_3)
    open func languageId(_ columnName: String) -> Self {
        self.languageId = columnName
        return self
    }

    /// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4)
    open func matchInfo(_ matchInfo: MatchInfo) -> Self {
        self.matchInfo = matchInfo
        return self
    }

    /// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options)
    open func order(_ order: Order) -> Self {
        self.order = order
        return self
    }

    override func options() -> Options {
        var options = super.options()
        for (column, _) in (columnDefinitions.filter { $0.options.contains(.unindexed) }) {
            options.append("notindexed", value: column)
        }
        options.append("languageid", value: languageId)
        options.append("compress", value: compressFunction)
        options.append("uncompress", value: uncompressFunction)
        options.append("matchinfo", value: matchInfo)
        options.append("order", value: order)
        return options
    }
}
