//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// Protocol that indicates concrete types conforming to it can be used a predicate member.
public protocol QueryPredicate: Evaluable {}

public enum QueryPredicateGroupType: String {
    case and
    case or
    case not
}

/// The `not` function is used to wrap a `QueryPredicate` in a `QueryPredicateGroup` of type `.not`.
/// - Parameter predicate: the `QueryPredicate` (either operation or group)
/// - Returns: `QueryPredicateGroup` of type `.not`
public func not<Predicate: QueryPredicate>(_ predicate: Predicate) -> QueryPredicateGroup {
    return QueryPredicateGroup(type: .not, predicates: [predicate])
}

/// The case `.all` is a predicate used as an argument to select all of a single modeltype. We
/// chose `.all` instead of `nil` because we didn't want to use the implicit nature of `nil` to
/// specify an action applies to an entire data set.
public enum QueryPredicateConstant: QueryPredicate {
    case all
    public func evaluate(target: Model) -> Bool {
        return true
    }
}

public class QueryPredicateGroup: QueryPredicate {
    public internal(set) var type: QueryPredicateGroupType
    public internal(set) var predicates: [QueryPredicate]

    public init(type: QueryPredicateGroupType = .and,
                predicates: [QueryPredicate] = []) {
        self.type = type
        self.predicates = predicates
    }

    public func and(_ predicate: QueryPredicate) -> QueryPredicateGroup {
        if case .and = type {
            predicates.append(predicate)
            return self
        }
        return QueryPredicateGroup(type: .and, predicates: [self, predicate])
    }

    public func or(_ predicate: QueryPredicate) -> QueryPredicateGroup {
        if case .or = type {
            predicates.append(predicate)
            return self
        }
        return QueryPredicateGroup(type: .or, predicates: [self, predicate])
    }

    public static func && (lhs: QueryPredicateGroup, rhs: QueryPredicate) -> QueryPredicateGroup {
        return lhs.and(rhs)
    }

    public static func || (lhs: QueryPredicateGroup, rhs: QueryPredicate) -> QueryPredicateGroup {
        return lhs.or(rhs)
    }

    public static prefix func ! (rhs: QueryPredicateGroup) -> QueryPredicateGroup {
        return not(rhs)
    }

    public func evaluate(target: Model) -> Bool {
        switch type {
        case .or:
            for predicate in predicates {
                if predicate.evaluate(target: target) {
                    return true
                }
            }
            return false
        case .and:
            for predicate in predicates {
                if !predicate.evaluate(target: target) {
                    return false
                }
            }
            return true
        case .not:
            let predicate = predicates[0]
            return !predicate.evaluate(target: target)
        }
    }
}

public class QueryPredicateOperation: QueryPredicate {

    public let field: String
    public let `operator`: QueryOperator

    public init(field: String, operator: QueryOperator) {
        self.field = field
        self.operator = `operator`
    }

    public func and(_ predicate: QueryPredicate) -> QueryPredicateGroup {
        let group = QueryPredicateGroup(type: .and, predicates: [self, predicate])
        return group
    }

    public func or(_ predicate: QueryPredicate) -> QueryPredicateGroup {
        let group = QueryPredicateGroup(type: .or, predicates: [self, predicate])
        return group
    }

    public static func && (lhs: QueryPredicateOperation, rhs: QueryPredicate) -> QueryPredicateGroup {
        return lhs.and(rhs)
    }

    public static func || (lhs: QueryPredicateOperation, rhs: QueryPredicate) -> QueryPredicateGroup {
        return lhs.or(rhs)
    }

    public static prefix func ! (rhs: QueryPredicateOperation) -> QueryPredicateGroup {
        return not(rhs)
    }

    public func evaluate(target: Model) -> Bool {
        guard let fieldValue = target[field] else {
            return false
        }

        guard let value = fieldValue else {
            return false
        }

        if let booleanValue = value as? Bool {
            return self.operator.evaluate(target: booleanValue)
        }

        if let doubleValue = value as? Double {
            return self.operator.evaluate(target: doubleValue)
        }

        if let intValue = value as? Int {
            return self.operator.evaluate(target: intValue)
        }

        if let timeValue = value as? Temporal.Time {
            return self.operator.evaluate(target: timeValue)
        }

        if let enumValue = value as? EnumPersistable {
            return self.operator.evaluate(target: enumValue.rawValue)
        }

        return self.operator.evaluate(target: value)
    }
}