//
//  CalendarModel.swift
//  KVKCalendar
//
//  Created by Sergei Kviatkovskii on 25.02.2020.
//

import UIKit
import EventKit

public enum TimeHourSystem: Int {
    @available(swift, deprecated: 0.3.6, obsoleted: 0.3.7, renamed: "twelve")
    case twelveHour = 0
    @available(swift, deprecated: 0.3.6, obsoleted: 0.3.7, renamed: "twentyFour")
    case twentyFourHour = 1
    
    case twelve = 12
    case twentyFour = 24
    
    var hours: [String] {
        switch self {
        case .twelveHour, .twelve:
            let array = ["12"] + Array(1...11).map({ String($0) })
            let am = array.map { $0 + " AM" } + ["Noon"]
            var pm = array.map { $0 + " PM" }
            
            pm.removeFirst()
            if let item = am.first {
                pm.append(item)
            }
            return am + pm
        case .twentyFourHour, .twentyFour:
            let array = ["00:00"] + Array(1...24).map({ (i) -> String in
                let i = i % 24
                var string = i < 10 ? "0" + "\(i)" : "\(i)"
                string.append(":00")
                return string
            })
            return array
        }
    }
    
    @available(*, deprecated, renamed: "current")
    public static var currentSystemOnDevice: TimeHourSystem? {
        let locale = NSLocale.current
        guard let formatter = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: locale) else { return nil }
        
        if formatter.contains("a") {
            return .twelve
        } else {
            return .twentyFour
        }
    }
    
    public static var current: TimeHourSystem? {
        let locale = NSLocale.current
        guard let formatter = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: locale) else { return nil }
        
        if formatter.contains("a") {
            return .twelve
        } else {
            return .twentyFour
        }
    }
    
    public var format: String {
        switch self {
        case .twelveHour, .twelve:
            return "h:mm a"
        case .twentyFourHour, .twentyFour:
            return "HH:mm"
        }
    }
}

public enum CalendarType: String, CaseIterable {
    case day, week, month, year, list
}

@available(swift, deprecated: 0.4.1, obsoleted: 0.4.2, renamed: "Event.Color")
public struct EventColor {
    let value: UIColor
    let alpha: CGFloat
    
    public init(_ color: UIColor, alpha: CGFloat = 0.3) {
        self.value = color
        self.alpha = alpha
    }
}

public struct Event {
    static let idForNewEvent = "-999"
    
    /// unique identifier of Event
    public var ID: String
    public var eventType: Int = 0 // support define: task, event, holiday, holiday is day off
    public var text: String = ""
    public var start: Date = Date()
    public var end: Date = Date()
    public var color: Event.Color? = Event.Color(.systemBlue) {
        didSet {
            guard let tempColor = color else { return }
            
            let value = prepareColor(tempColor)
            backgroundColor = value.background
            textColor = value.text
        }
    }
    public var backgroundColor: UIColor = UIColor.systemBlue.withAlphaComponent(0.3)
    public var textColor: UIColor = .white
    public var isAllDay: Bool = false
    public var isContainsFile: Bool = false
    public var textForMonth: String = ""
    public var textForList: String = ""
    public var createAt: Date = Date()
    
    @available(swift, deprecated: 0.4.6, obsoleted: 0.4.7, renamed: "data")
    public var eventData: Any? = nil
    public var data: Any? = nil
    public var canEdit: Bool = true
    
    public var recurringType: Event.RecurringType = .none
    
    ///individual event customization
    ///(in-progress) works only a default height
    public var style: EventStyle? = nil
    
    public init(ID: String) {
        self.ID = ID
        
        if let tempColor = color {
            let value = prepareColor(tempColor)
            backgroundColor = value.background
            textColor = value.text
        }
    }
    
    func prepareColor(_ color: Event.Color) -> (background: UIColor, text: UIColor) {
        let bgColor = color.value.withAlphaComponent(color.alpha)
        var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0
        color.value.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
        let txtColor = UIColor(hue: hue, saturation: saturation,
                               brightness: UIScreen.isDarkMode ? brightness : brightness * 0.4,
                               alpha: alpha)
        
        return (bgColor, txtColor)
    }
}

extension Event {
    var hash: Int {
        return ID.hashValue
    }
    
    func clone() -> Event {
        return self
    }
}

public extension Event {
    var isNew: Bool {
        return ID == Event.idForNewEvent
    }
    
    enum RecurringType: Int {
        case everyDay, everyWeek, everyMonth, everyYear, none
    }
    
    struct Color {
        let value: UIColor
        let alpha: CGFloat
        
        public init(_ color: UIColor, alpha: CGFloat = 0.3) {
            self.value = color
            self.alpha = alpha
        }
    }
}

@available(swift, deprecated: 0.4.1, obsoleted: 0.4.2, renamed: "Event.RecurringType")
public enum RecurringType: Int {
    case everyDay, everyWeek, everyMonth, everyYear, none
}

extension Event: EventProtocol {
    public func compare(_ event: Event) -> Bool {
        return hash == event.hash
    }
}

extension Calendar {
    func numberOfDaysBetween(_ from: Date, and to: Date) -> Int {
        let fromDate = startOfDay(for: from) // <1>
        let toDate = startOfDay(for: to) // <2>
        let numberOfDays = dateComponents([.day], from: fromDate, to: toDate) // <3>
        return numberOfDays.day!
    }
}

extension Date {
    func isLessThanDate(_ date: Date) -> Bool {
        return (self.compare(date) == .orderedAscending) && date != self
    }
    func isGreaterThanDate(_ date: Date) -> Bool {
        return (self.compare(date) == .orderedDescending) && date != self
    }
    var startOfDayOfDate: Date {
        return Calendar.current.startOfDay(for: self)
    }
    var endOfDayOfDate: Date {
        var components = DateComponents()
        components.day = 1
        components.second = -1
        return Calendar.current.date(byAdding: components, to: self.startOfDayOfDate)!
    }
}

extension Event {
    /*
     Tong quat event bat dau tu : a1 -> a2 loop every day
     Ngay thu an: (an > a2) 1 end event + (an >= a2) (a2 - a1 -1) full day + 1 start
     */
    func updateListDateRepeatEveryDay(newDate: Date?, calendar: Calendar = Calendar.current) -> [Event]? {
        guard let newDate = newDate, recurringType == .everyDay else {// chi tinh cho lap ngay
            return [Event]()
        }
        if self.start.startOfDayOfDate.isGreaterThanDate(newDate.startOfDayOfDate) {// nhung ngay truoc do
            return [Event]()
        }
        if calendar.numberOfDaysBetween(self.start.startOfDayOfDate, and: newDate.startOfDayOfDate) == 0 {// cung ngay bat dau
            return [Event]()
        }
        var amongEvent = calendar.numberOfDaysBetween(self.start, and: self.end)
        amongEvent = abs(amongEvent)
        var result = [Event]()
        var startComponents = DateComponents()
        startComponents.year = newDate.year
        startComponents.month = newDate.month
        startComponents.hour = start.hour
        startComponents.minute = start.minute
        startComponents.day = newDate.day
        
        var endComponents = DateComponents()
        endComponents.year = newDate.year
        endComponents.month = newDate.month
        endComponents.hour = end.hour
        endComponents.minute = end.minute
        endComponents.day = newDate.day
        if amongEvent < 1 {// khong qua ngay hom sau
            guard let newStart = calendar.date(from: startComponents), let newEnd = calendar.date(from: endComponents) else { return nil }
            var newEvent = self
            newEvent.start = newStart
            newEvent.end = newEnd
            return [newEvent]
        }
        if newDate.startOfDayOfDate.isGreaterThanDate(self.end.startOfDayOfDate) { //an > a2
            var itemCopy = self.clone()
            itemCopy.start = newDate.startOfDayOfDate
            let newEnd = calendar.date(from: endComponents) ?? end
            itemCopy.end = newEnd
            result.append(itemCopy)
        }
        
        if self.start.startOfDayOfDate.isLessThanDate(newDate.startOfDayOfDate) { // an >= a2
            var amongDateReferNew = calendar.numberOfDaysBetween(self.start.startOfDayOfDate, and: newDate.startOfDayOfDate)
            amongDateReferNew = amongDateReferNew > amongEvent ? amongEvent : amongDateReferNew
            let needLoop = amongDateReferNew - 1
            if needLoop > 0 {
                for _ in 0..<needLoop {
                    var itemCopy = self.clone()
                    itemCopy.start = newDate.startOfDayOfDate
                    itemCopy.end = newDate.endOfDayOfDate
                    result.append(itemCopy)
                }
            }
        }
        var itemCopy = self.clone()
        let newStart = calendar.date(from: startComponents) ?? start
        itemCopy.start = newStart
        itemCopy.end = newDate.endOfDayOfDate
        result.append(itemCopy)
        return result
    }
    
    func updateDate(newDate: Date?, calendar: Calendar = Calendar.current) -> Event? {
        var startComponents = DateComponents()
        startComponents.year = newDate?.year
        startComponents.month = newDate?.month
        startComponents.hour = start.hour
        startComponents.minute = start.minute
        
        var endComponents = DateComponents()
        endComponents.year = newDate?.year
        endComponents.month = newDate?.month
        endComponents.hour = end.hour
        endComponents.minute = end.minute
        
        switch recurringType {
        case .everyDay:
            startComponents.day = newDate?.day
        case .everyWeek where newDate?.weekday == start.weekday:
            startComponents.day = newDate?.day
            startComponents.weekday = newDate?.weekday
            endComponents.weekday = newDate?.weekday
        case .everyMonth where newDate?.month != start.month && newDate?.day == start.day:
            startComponents.day = newDate?.day
        case .everyYear where newDate?.year != start.year && newDate?.month == start.month && newDate?.day == start.day:
            startComponents.day = newDate?.day
        default:
            return nil
        }
        
        let offsetDay = end.day - start.day
        if start.day == end.day {
            endComponents.day = newDate?.day
        } else if let newDay = newDate?.day {
            endComponents.day = newDay + offsetDay
        } else {
            endComponents.day = newDate?.day
        }
        
        guard let newStart = calendar.date(from: startComponents), let newEnd = calendar.date(from: endComponents) else { return nil }
        
        var newEvent = self
        newEvent.start = newStart
        newEvent.end = newEnd
        return newEvent
    }
}

// MARK: - Event protocol

public protocol EventProtocol {
    func compare(_ event: Event) -> Bool
}

// MARK: - Settings protocol

protocol CalendarSettingProtocol: AnyObject {
    func reloadFrame(_ frame: CGRect)
    func updateStyle(_ style: StyleKVK)
    func reloadData(_ events: [Event])
    func setDate(_ date: Date)
    func setUI()
}

extension CalendarSettingProtocol {
    func setUI() {}
    func updateStyle(_ style: StyleKVK) {}
    func reloadData(_ events: [Event]) {}
    func setDate(_ date: Date) {}
}

// MARK: - Data source protocol

public protocol CalendarDataSource: AnyObject {
    /// get events to display on view
    /// also this method returns a system events from iOS calendars if you set the property `systemCalendar` in style
    func eventsForCalendar(systemEvents: [EKEvent]) -> [Event]
    
    func willDisplayDate(_ date: Date?, events: [Event])
    
    /// Use this method to add a custom event view
    func willDisplayEventView(_ event: Event, frame: CGRect, date: Date?) -> EventViewGeneral?
    
    /// Use this method to add a custom header view (works on Day, Week, Month)
    func willDisplayHeaderSubview(date: Date?, frame: CGRect, type: CalendarType) -> UIView?
    
    /// Use the method to replace the collectionView. Works for month/year View
    func willDisplayCollectionView(frame: CGRect, type: CalendarType) -> UICollectionView?
    
    func willDisplayEventViewer(date: Date, frame: CGRect) -> UIView?
    
    /// Use this method to add a custom day cell **DEPRECATED**
    @available(*, deprecated, renamed: "dequeueCell")
    func dequeueDateCell(date: Date?, type: CalendarType, collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell?
    
    /// **DEPRECATED**
    @available(*, deprecated, renamed: "dequeueHeader")
    func dequeueHeaderView(date: Date?, type: CalendarType, collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionReusableView?
    
    /// **DEPRECATED**
    @available(*, deprecated, renamed: "dequeueCell")
    func dequeueListCell(date: Date?, tableView: UITableView, indexPath: IndexPath) -> UITableViewCell?
    
    /// Use this method to add a custom day cell
    func dequeueCell<T: UIScrollView>(date: Date?, type: CalendarType, view: T, indexPath: IndexPath, events: [Event]) -> KVKCalendarCellProtocol?
    
    func dequeueNibCell<T: UIScrollView>(date: Date?, type: CalendarType, view: T, indexPath: IndexPath, events: [Event]) -> KVKCalendarCellProtocol?
    
    /// Use this method to add a header view
    func dequeueHeader<T: UIScrollView>(date: Date?, type: CalendarType, view: T, indexPath: IndexPath) -> KVKCalendarHeaderProtocol?
}

public extension CalendarDataSource {
    func willDisplayEventViewer(date: Date, frame: CGRect) -> UIView? { return nil }
    
    func willDisplayDate(_ date: Date?, events: [Event]) {}
    
    func willDisplayEventView(_ event: Event, frame: CGRect, date: Date?) -> EventViewGeneral? { return nil }
    
    func willDisplayHeaderSubview(date: Date?, frame: CGRect, type: CalendarType) -> UIView? { return nil }
    
    func willDisplayCollectionView(frame: CGRect, type: CalendarType) -> UICollectionView? { return nil }
    
    /// **DEPRECATED**
    @available(*, deprecated, renamed: "dequeueCell")
    func dequeueDateCell(date: Date?, type: CalendarType, collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell? { return nil }
    
    /// **DEPRECATED**
    @available(*, deprecated, renamed: "dequeueHeader")
    func dequeueHeaderView(date: Date?, type: CalendarType, collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionReusableView? { return nil }
        
    /// **DEPRECATED**
    @available(*, deprecated, renamed: "dequeueCell")
    func dequeueListCell(date: Date?, tableView: UITableView, indexPath: IndexPath) -> UITableViewCell? { return nil }
    
    func dequeueCell<T: UIScrollView>(date: Date?, type: CalendarType, view: T, indexPath: IndexPath, events: [Event]) -> KVKCalendarCellProtocol? { return nil }
    
    func dequeueNibCell<T: UIScrollView>(date: Date?, type: CalendarType, view: T, indexPath: IndexPath, events: [Event]) -> KVKCalendarCellProtocol? { return nil }
    
    func dequeueHeader<T: UIScrollView>(date: Date?, type: CalendarType, view: T, indexPath: IndexPath) -> KVKCalendarHeaderProtocol? { return nil }
}

// MARK: - Delegate protocol

public protocol CalendarDelegate: AnyObject {
    
    func monthChangee(_ date: Date?, type: CalendarType)
    
    func sizeForHeader(_ date: Date?, type: CalendarType) -> CGSize?
    
    /// size cell for (month, year, list) view
    func sizeForCell(_ date: Date?, type: CalendarType) -> CGSize?
    
    /// get a selecting date
    @available(*, deprecated, renamed: "didSelectDates")
    func didSelectDate(_ date: Date?, type: CalendarType, frame: CGRect?)
    
    /// get selected dates
    func didSelectDates(_ dates: [Date], type: CalendarType, frame: CGRect?)
    
    /// get a selecting event
    func didSelectEvent(_ event: Event, type: CalendarType, frame: CGRect?)
    
    func didAddEventList(_ date: Date)
    
    /// tap on more fro month view
    func didSelectMore(_ date: Date, frame: CGRect?)
    
    /// event's viewer for iPad
    /// **DEPRECATED**
    @available(*, deprecated, renamed: "didChangeViewerFrame")
    func eventViewerFrame(_ frame: CGRect)
    
    /// event's viewer for iPad
    func didChangeViewerFrame(_ frame: CGRect)
    
    /// drag & drop events and resize
    func didChangeEvent(_ event: Event, start: Date?, end: Date?)
    
    /// add new event
    func didAddNewEvent(_ event: Event, _ date: Date?)
    
    /// get current displaying events
    func didDisplayEvents(_ events: [Event], dates: [Date?])
    
    /// get next date when the calendar scrolls (works for month view)
    func willSelectDate(_ date: Date, type: CalendarType)
    
    /// **DEPRECATED**
    @available(*, deprecated, renamed: "didDeselectEvent")
    func deselectEvent(_ event: Event, animated: Bool)
    
    /// deselect event on timeline
    func didDeselectEvent(_ event: Event, animated: Bool)
}

public extension CalendarDelegate {
    func sizeForHeader(_ date: Date?, type: CalendarType) -> CGSize? { return nil }
    
    func sizeForCell(_ date: Date?, type: CalendarType) -> CGSize? { return nil }
    
    @available(*, deprecated, renamed: "didSelectDates")
    func didSelectDate(_ date: Date?, type: CalendarType, frame: CGRect?) {}
    
    func didSelectDates(_ dates: [Date], type: CalendarType, frame: CGRect?)  {}
    
    func didSelectEvent(_ event: Event, type: CalendarType, frame: CGRect?) {}
    
    func didSelectMore(_ date: Date, frame: CGRect?) {}
    
    func eventViewerFrame(_ frame: CGRect) {}
    
    func didChangeEvent(_ event: Event, start: Date?, end: Date?) {}
        
    func didAddNewEvent(_ event: Event, _ date: Date?) {}
    
    func didDisplayEvents(_ events: [Event], dates: [Date?]) {}
    
    func willSelectDate(_ date: Date, type: CalendarType) {}
    
    @available(*, deprecated, renamed: "didDeselectEvent")
    func deselectEvent(_ event: Event, animated: Bool) {}
    
    func didDeselectEvent(_ event: Event, animated: Bool) {}
    
    func didChangeViewerFrame(_ frame: CGRect) {}
}

// MARK: - Private Display dataSource

protocol DisplayDataSource: CalendarDataSource {}

extension DisplayDataSource {
    public func eventsForCalendar(systemEvents: [EKEvent]) -> [Event] { return [] }
}

// MARK: - Private Display delegate

protocol DisplayDelegate: CalendarDelegate {
    func didDisplayEvents(_ events: [Event], dates: [Date?], type: CalendarType)
}

extension DisplayDelegate {
    public func willSelectDate(_ date: Date, type: CalendarType) {}
    
    func deselectEvent(_ event: Event, animated: Bool) {}
}

// MARK: - EKEvent

public extension EKEvent {
    func transform(text: String? = nil, textForMonth: String? = nil, textForList: String? = nil) -> Event {
        var event = Event(ID: eventIdentifier)
        event.text = text ?? title
        event.start = startDate
        event.end = endDate
        event.color = Event.Color(UIColor(cgColor: calendar.cgColor))
        event.isAllDay = isAllDay
        event.textForMonth = textForMonth ?? title
        event.textForList = textForList ?? title
        return event
    }
}

// MARK: - Protocols to customize calendar

public protocol KVKCalendarCellProtocol: AnyObject {}

extension UICollectionViewCell: KVKCalendarCellProtocol {}
extension UITableViewCell: KVKCalendarCellProtocol {}

public protocol KVKCalendarHeaderProtocol: AnyObject {}

extension UIView: KVKCalendarHeaderProtocol {}

