
import Foundation
enum SignificantEventsDecodeError: Error {
    case unableToParseIntoTypedEvents
}

public struct SignificantEvents: Decodable {
    public let nextRvStartId: UInt?
    public let sha: String?
    private let untypedEvents: [UntypedEvent]
    public let typedEvents: [TypedEvent]
    public let summary: Summary
    
    enum CodingKeys: String, CodingKey {
        case nextRvStartId
        case sha
        case untypedEvents = "timeline"
        case typedEvents
        case summary
    }
    
    public struct Summary: Decodable {
        public let earliestTimestampString: String
        public let numChanges: UInt
        public let numUsers: UInt
        
        enum CodingKeys: String, CodingKey {
            case earliestTimestampString = "earliestTimestamp"
            case numChanges
            case numUsers
        }
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        nextRvStartId = try? container.decode(UInt.self, forKey: .nextRvStartId)
        sha = try? container.decode(String.self, forKey: .sha)
        summary = try container.decode(Summary.self, forKey: .summary)
        let untypedEvents = try container.decode([UntypedEvent].self, forKey: .untypedEvents)
        
        var typedEvents: [TypedEvent] = []
        
        for untypedEvent in untypedEvents {
            switch untypedEvent.outputType {
            case .small:
                if let event = Event.Small(untypedEvent: untypedEvent) {
                    typedEvents.append(.small(event))
                }
            case .large:
                if let event = Event.Large(untypedEvent: untypedEvent) {
                    typedEvents.append(.large(event))
                }
            case .vandalismRevert:
                if let event = Event.VandalismRevert(untypedEvent: untypedEvent) {
                    typedEvents.append(.vandalismRevert(event))
                }
            case .newTalkPageTopic:
                if let event = Event.NewTalkPageTopic(untypedEvent: untypedEvent) {
                    typedEvents.append(.newTalkPageTopic(event))
                }
            }
        }

        //zero untyped events is a valid case if the user has paged to the end of the endpoint cache
        //unTypedEvents > 0 and typedEvents == 0 is invalid, meaning all events failed to convert
        guard typedEvents.count > 0 || untypedEvents.count == 0 else {
            throw SignificantEventsDecodeError.unableToParseIntoTypedEvents
        }
        
        self.typedEvents = typedEvents
        self.untypedEvents = untypedEvents
    }
    
    public enum SnippetType: Int, Decodable {
        case addedLine = 1
        case addedAndDeletedInLine = 3
        case addedAndDeletedInMovedLine = 5
    }
    
    public enum EventOutputType: String, Decodable {
        case large = "large-change"
        case small = "small-change"
        case newTalkPageTopic = "new-talk-page-topic"
        case vandalismRevert = "vandalism-revert"
    }
    
    public enum ChangeOutputType: String, Decodable {
        case addedText = "added-text"
        case deletedText = "deleted-text"
        case newTemplate = "new-template"
    }
    
    public enum TypedEvent {
        case large(Event.Large)
        case small(Event.Small)
        case vandalismRevert(Event.VandalismRevert)
        case newTalkPageTopic(Event.NewTalkPageTopic)
    }
    
    public enum TypedChange {
        case addedText(Change.AddedText)
        case deletedText(Change.DeletedText)
        case newTemplate(Change.NewTemplates)
    }
}

//MARK: Events

public extension SignificantEvents {
    
    struct Event {
        
        public struct Large {
            let outputType: EventOutputType
            public let revId: UInt
            public let parentId: UInt
            public let timestampString: String
            public let user: String
            public let userId: UInt
            public let userGroups: [String]?
            public let userEditCount: UInt?
            public let typedChanges: [TypedChange]
            
            init?(untypedEvent: UntypedEvent) {
                guard let revId = untypedEvent.revId,
                      let parentId = untypedEvent.parentId,
                      let timestampString = untypedEvent.timestampString,
                      let user = untypedEvent.user,
                      let userId = untypedEvent.userId,
                      let untypedChanges = untypedEvent.untypedChanges else {
                    return nil
                }
                
                self.outputType = untypedEvent.outputType
                self.revId = revId
                self.parentId = parentId
                self.timestampString = timestampString
                self.user = user
                self.userId = userId
                self.userGroups = untypedEvent.userGroups
                self.userEditCount = untypedEvent.userEditCount
                
                var changes: [TypedChange] = []
                
                for untypedChange in untypedChanges {
                    switch untypedChange.outputType {
                    case .addedText:
                        if let change = Change.AddedText(untypedChange: untypedChange) {
                            changes.append(.addedText(change))
                        }
                    case .deletedText:
                        if let change = Change.DeletedText(untypedChange: untypedChange) {
                            changes.append(.deletedText(change))
                        }
                    case .newTemplate:
                        if let change = Change.NewTemplates(untypedChange: untypedChange) {
                            changes.append(.newTemplate(change))
                        }
                    }
                }
                
                guard changes.count == untypedChanges.count else {
                    return nil
                }
                
                self.typedChanges = changes
            }
        }
        
        public struct Small: Equatable {
            let outputType: EventOutputType
            public let revId: UInt
            public let parentId: UInt
            public let timestampString: String
            
            fileprivate init?(untypedEvent: UntypedEvent) {
                guard let revId = untypedEvent.revId,
                      let parentId = untypedEvent.parentId,
                      let timestampString = untypedEvent.timestampString else {
                    return nil
                }
                
                self.outputType = untypedEvent.outputType
                self.revId = revId
                self.parentId = parentId
                self.timestampString = timestampString
            }
            
            public static func == (lhs: SignificantEvents.Event.Small, rhs: SignificantEvents.Event.Small) -> Bool {
                return lhs.revId == rhs.revId
            }
        }
        
        public struct VandalismRevert {
            let outputType: EventOutputType
            public let revId: UInt
            public let parentId: UInt
            public let timestampString: String
            public let user: String
            public let userId: UInt
            public let sections: [String]
            public let userGroups: [String]?
            public let userEditCount: UInt?
            
            fileprivate init?(untypedEvent: UntypedEvent) {
                guard let revId = untypedEvent.revId,
                      let parentId = untypedEvent.parentId,
                      let timestampString = untypedEvent.timestampString,
                      let user = untypedEvent.user,
                      let userId = untypedEvent.userId,
                      let sections = untypedEvent.sections else {
                    return nil
                }
                
                self.outputType = untypedEvent.outputType
                self.revId = revId
                self.parentId = parentId
                self.timestampString = timestampString
                self.user = user
                self.userId = userId
                self.sections = sections
                self.userGroups = untypedEvent.userGroups
                self.userEditCount = untypedEvent.userEditCount
            }
        }
        
        public struct NewTalkPageTopic {
            let outputType: EventOutputType
            let revId: UInt
            let parentId: UInt
            public let timestampString: String
            public let user: String
            public let userId: UInt
            public let section: String?
            public let snippet: String
            public let userGroups: [String]?
            public let userEditCount: UInt?
            
            fileprivate init?(untypedEvent: UntypedEvent) {
                guard let revId = untypedEvent.revId,
                      let parentId = untypedEvent.parentId,
                      let timestampString = untypedEvent.timestampString,
                      let user = untypedEvent.user,
                      let userId = untypedEvent.userId,
                      let snippet = untypedEvent.snippet else {
                    return nil
                }
                
                self.outputType = untypedEvent.outputType
                self.revId = revId
                self.parentId = parentId
                self.timestampString = timestampString
                self.user = user
                self.userId = userId
                self.section = untypedEvent.section
                self.snippet = snippet
                self.userGroups = untypedEvent.userGroups
                self.userEditCount = untypedEvent.userEditCount
            }
        }
    }
}

//MARK: Changes

public extension SignificantEvents {
    
    struct Change {
        
        public struct AddedText {
            let outputType: ChangeOutputType
            public let sections: [String]
            public let snippet: String?
            public let snippetType: SnippetType
            public let characterCount: UInt
            
            fileprivate init?(untypedChange: UntypedChange) {
                guard let snippetType = untypedChange.snippetType,
                      let characterCount = untypedChange.characterCount else {
                    return nil
                }
                
                self.outputType = untypedChange.outputType
                self.sections = untypedChange.sections
                self.snippet = untypedChange.snippet
                self.snippetType = snippetType
                self.characterCount = characterCount
            }
        }
        
        public struct DeletedText {
            let outputType: ChangeOutputType
            public let sections: [String]
            public let characterCount: UInt
            
            fileprivate init?(untypedChange: UntypedChange) {
                guard let characterCount = untypedChange.characterCount else {
                    return nil
                }
                
                self.outputType = untypedChange.outputType
                self.sections = untypedChange.sections
                self.characterCount = characterCount
            }
        }
        
        public struct NewTemplates {
            let outputType: ChangeOutputType
            public let sections: [String]
            private let untypedTemplates: [[String: String]]
            public let typedTemplates: [Template]
            
            fileprivate init?(untypedChange: UntypedChange) {
                guard let untypedTemplates = untypedChange.untypedTemplates else {
                    return nil
                }
                
                var typedTemplates: [Template] = []
                self.outputType = untypedChange.outputType
                self.sections = untypedChange.sections
                self.untypedTemplates = untypedTemplates
                
                for untypedTemplate in untypedTemplates {
                    guard let name = untypedTemplate["name"] else {
                        continue
                    }
                    if name.localizedCaseInsensitiveContains("cite") {
                        if name.localizedCaseInsensitiveContains("book"), let bookCitation = Citation.Book(dict: untypedTemplate) {
                            typedTemplates.append(.bookCitation(bookCitation))
                        } else if name.localizedCaseInsensitiveContains("journal"), let journalCitation = Citation.Journal(dict: untypedTemplate) {
                            typedTemplates.append(.journalCitation(journalCitation))
                        } else if name.localizedCaseInsensitiveContains("web"), let webCitation = Citation.Website(dict: untypedTemplate) {
                            typedTemplates.append(.websiteCitation(webCitation))
                        } else if name.localizedCaseInsensitiveContains("news"), let newsCitation = Citation.News(dict: untypedTemplate) {
                            typedTemplates.append(.newsCitation(newsCitation))
                        }
                    } else if name.localizedCaseInsensitiveContains("short description"), let articleDescription = ArticleDescription(dict: untypedTemplate) {
                        typedTemplates.append(.articleDescription(articleDescription))
                    }
                }
                
                self.typedTemplates = typedTemplates
            }
        }
    }
}

//MARK: Templates

public extension SignificantEvents {
    
    enum Template {
        case bookCitation(Citation.Book)
        case articleDescription(ArticleDescription)
        case journalCitation(Citation.Journal)
        case newsCitation(Citation.News)
        case websiteCitation(Citation.Website)
    }
    
    struct Citation {
        
        //https://en.wikipedia.org/wiki/Template:Cite_book/TemplateData
        public struct Book {
            public let title: String
            public let lastName: String?
            public let firstName: String?
            public let yearPublished: String?
            public let locationPublished: String?
            public let publisher: String?
            public let pagesCited: String?
            public let isbn: String?
            
            init?(dict: [String: String]) {
                guard let title = dict.nonEmptyValueForKey(key: "title") else {
                    return nil
                }
                
                self.title = title
                
                let batch1 = dict.nonEmptyValueForKey(key: "last") ??
                    dict.nonEmptyValueForKey(key: "last1") ??
                    dict.nonEmptyValueForKey(key: "author") ??
                    dict.nonEmptyValueForKey(key: "author1") ??
                    dict.nonEmptyValueForKey(key: "author1-last")
                let batch2 = dict.nonEmptyValueForKey(key: "author-last") ??
                    dict.nonEmptyValueForKey(key: "surname1") ??
                    dict.nonEmptyValueForKey(key: "author-last1") ??
                    dict.nonEmptyValueForKey(key: "subject1") ??
                    dict.nonEmptyValueForKey(key: "surname")
                let batch3 = dict.nonEmptyValueForKey(key: "author-last") ??
                    dict.nonEmptyValueForKey(key: "subject")
                
                self.lastName = batch1 ?? batch2 ?? batch3
                
                self.firstName = dict.nonEmptyValueForKey(key: "first") ??
                                dict.nonEmptyValueForKey(key: "given") ??
                                dict.nonEmptyValueForKey(key: "author-first") ??
                                dict.nonEmptyValueForKey(key: "first1") ??
                                dict.nonEmptyValueForKey(key: "given1") ??
                                dict.nonEmptyValueForKey(key: "author-first1") ??
                                dict.nonEmptyValueForKey(key: "author1-first")
                
                self.yearPublished = dict.nonEmptyValueForKey(key: "year")
                self.locationPublished = dict.nonEmptyValueForKey(key: "location") ??
                                            dict.nonEmptyValueForKey(key: "place")
                
                self.publisher = dict.nonEmptyValueForKey(key: "publisher") ??
                                dict.nonEmptyValueForKey(key: "distributor") ??
                                dict.nonEmptyValueForKey(key: "institution") ??
                                dict.nonEmptyValueForKey(key: "newsgroup")
                
                self.pagesCited = dict.nonEmptyValueForKey(key: "pages") ??
                    dict.nonEmptyValueForKey(key: "pp")
                
                self.isbn = dict.nonEmptyValueForKey(key: "isbn", caseInsensitive: true) ??
                            dict.nonEmptyValueForKey(key: "isbn13", caseInsensitive: true)
            }
        }
        
        //https://en.wikipedia.org/wiki/Template:Cite_journal#TemplateData
        public struct Journal {
            public let lastName: String?
            public let firstName: String?
            public let sourceDateString: String?
            public let title: String
            public let journal: String
            public let urlString: String?
            public let volumeNumber: String?
            public let pages: String?
            public let database: String?
            
            init?(dict: [String: String]) {
                guard let title = dict.nonEmptyValueForKey(key: "title"),
                let journal = dict.nonEmptyValueForKey(key: "journal") else {
                    return nil
                }
                
                self.title = title
                self.journal = journal
                
                self.lastName = dict.nonEmptyValueForKey(key: "last") ??
                dict.nonEmptyValueForKey(key: "author") ??
                dict.nonEmptyValueForKey(key: "author1") ??
                dict.nonEmptyValueForKey(key: "authors") ??
                dict.nonEmptyValueForKey(key: "last1")
                
                self.firstName = dict.nonEmptyValueForKey(key: "first") ??
                dict.nonEmptyValueForKey(key: "first1")
                
                self.sourceDateString = dict.nonEmptyValueForKey(key: "date")
                self.urlString = dict.nonEmptyValueForKey(key: "url", caseInsensitive: true)
                self.volumeNumber = dict.nonEmptyValueForKey(key: "volume")
                self.pages = dict.nonEmptyValueForKey(key: "pages")
                self.database = dict.nonEmptyValueForKey(key: "via")
            }
        }
        
        //https://en.wikipedia.org/wiki/Template:Cite_news#TemplateData
        public struct News {
            public let lastName: String?
            public let firstName: String?
            public let sourceDateString: String?
            public let title: String
            public let urlString: String?
            public let publication: String?
            public let accessDateString: String?
            
            init?(dict: [String: String]) {
                guard let title = dict.nonEmptyValueForKey(key: "title") else {
                    return nil
                }
                
                self.title = title
                self.lastName = dict.nonEmptyValueForKey(key: "last") ??
                                dict.nonEmptyValueForKey(key: "last1") ??
                                dict.nonEmptyValueForKey(key: "author") ??
                                dict.nonEmptyValueForKey(key: "author1") ??
                                dict.nonEmptyValueForKey(key: "authors")
                
                self.firstName = dict.nonEmptyValueForKey(key: "first") ??
                                dict.nonEmptyValueForKey(key: "first1")
                
                self.sourceDateString = dict.nonEmptyValueForKey(key: "date")
                self.publication = dict.nonEmptyValueForKey(key: "work") ??
                                    dict.nonEmptyValueForKey(key: "journal") ??
                                    dict.nonEmptyValueForKey(key: "magazine") ??
                                    dict.nonEmptyValueForKey(key: "periodical") ??
                                    dict.nonEmptyValueForKey(key: "newspaper") ??
                                    dict.nonEmptyValueForKey(key: "website")
                
                self.urlString = dict.nonEmptyValueForKey(key: "url", caseInsensitive: true)
                self.accessDateString = dict.nonEmptyValueForKey(key: "access-date") ?? dict.nonEmptyValueForKey(key: "accessdate")
            }
        }
        
        //https://en.wikipedia.org/wiki/Template:Cite_web#TemplateData
        public struct Website {
            
            public let urlString: String
            public let title: String
            public let publisher: String?
            public let accessDateString: String?
            public let archiveDateString: String?
            public let archiveDotOrgUrlString: String?
            
            init?(dict: [String: String]) {
                guard let title = dict.nonEmptyValueForKey(key: "title"),
                      let urlString = dict.nonEmptyValueForKey(key: "url", caseInsensitive: true) else {
                    return nil
                }
                
                self.title = title
                self.urlString = urlString
                
                self.publisher = dict.nonEmptyValueForKey(key: "publisher") ??
                                dict.nonEmptyValueForKey(key: "website") ??
                                dict.nonEmptyValueForKey(key: "work")
                
                self.accessDateString = dict.nonEmptyValueForKey(key: "access-date") ?? dict.nonEmptyValueForKey(key: "accessdate")
                self.archiveDateString = dict.nonEmptyValueForKey(key: "archive-date") ?? dict.nonEmptyValueForKey(key: "archivedate")
                self.archiveDotOrgUrlString = dict.nonEmptyValueForKey(key: "archive-url") ?? dict.nonEmptyValueForKey(key: "archiveurl")
            }
        }
    }
    
    struct ArticleDescription {
        public let text: String
        
        init?(dict: [String: String]) {
            guard let text = dict.nonEmptyValueForKey(key: "1") else {
                return nil
            }
            
            self.text = text
        }
    }
    
    
}

//MARK: Untyped

public extension SignificantEvents {
    struct UntypedEvent: Decodable {
        let outputType: EventOutputType
        let revId: UInt?
        let parentId: UInt?
        let timestampString: String?
        let user: String?
        let userId: UInt?
        let userGroups: [String]?
        let userEditCount: UInt?
        let count: UInt?
        let sections: [String]?
        let section: String?
        let snippet: String?
        let untypedChanges: [UntypedChange]?
        
        enum CodingKeys: String, CodingKey {
                case revId = "revid"
                case parentId = "parentid"
                case timestampString = "timestamp"
                case outputType
                case user
                case userId = "userid"
                case userGroups
                case userEditCount
                case count
                case sections
                case section
                case snippet
                case untypedChanges = "significantChanges"
            }
    }
    
    struct UntypedChange: Decodable {
        let outputType: ChangeOutputType
        let sections: [String]
        let snippet: String?
        let snippetType: SnippetType?
        let characterCount: UInt?
        let untypedTemplates: [[String: String]]?
        
        enum CodingKeys: String, CodingKey {
            case outputType
            case sections
            case snippet
            case snippetType
            case characterCount
            case untypedTemplates = "templates"
        }
    }
}

private extension Dictionary where Key == String, Value == String {
    func nonEmptyValueForKey(key: String, caseInsensitive: Bool = false) -> String? {
        guard let key = caseInsensitive
                ? keys.first(where: {$0.caseInsensitiveCompare(key) == .orderedSame})
                : key else {
                    return nil
         }

        if let value = self[key], !value.isEmpty {
            return value
        }

        return nil
    }
}
