//
//  TimelineView.swift
//  KVKCalendar
//
//  Created by Sergei Kviatkovskii on 02/01/2019.
//

import UIKit

final class TimelineView: UIView, EventDateProtocol {
    
    weak var delegate: TimelineDelegate?
    weak var dataSource: DisplayDataSource?
    var style: Style
    var eventPreview: UIView?
    var autoScrolledToCurrentTime = false
    var autoScrolledToFirstEvent = false
    var eventPreviewSize = CGSize(width: 100, height: 100)
    
    private(set) var tagCurrentHourLine = -10
    private(set) var tagEventPagePreview = -20
    private(set) var tagVerticalLine = -30
    private let tagShadowView = -40
    private let tagBackgroundView = -50
    private let tagAllDayPlaceholder = -60
    private let tagAllDayEvent = -70
    
    private let hours: [String]
    private let timeHourSystem: TimeHourSystem
    private var timer: Timer?
    private var dates = [Date?]()
    private var selectedDate: Date?
    private(set) var type: CalendarType
    
    private(set) lazy var shadowView: UIView = {
        let view = UIView()
        view.backgroundColor = style.timeline.shadowColumnColor
        view.alpha = style.timeline.shadowColumnAlpha
        view.tag = tagShadowView
        return view
    }()
    
    private lazy var currentTimeLabel: TimelineLabel = {
        let label = TimelineLabel()
        label.tag = tagCurrentHourLine
        label.textColor = style.timeline.currentLineHourColor
        label.textAlignment = .center
        label.font = style.timeline.currentLineHourFont
        label.adjustsFontSizeToFitWidth = true
        label.backgroundColor = UIColor.white.withAlphaComponent(0.8)
        let formatter = DateFormatter()
        formatter.dateFormat = timeHourSystem.format
        label.text = formatter.string(from: Date())
        label.valueHash = Date().minute.hashValue
        return label
    }()
    
    private(set) lazy var movingMinutesLabel: TimelineLabel = {
        let label = TimelineLabel()
        label.adjustsFontSizeToFitWidth = true
        label.textColor = style.timeline.movingMinutesColor
        label.textAlignment = .right
        label.font = style.timeline.timeFont
        return label
    }()
    
    private lazy var currentLineView: UIView = {
        let view = UIView()
        view.tag = tagCurrentHourLine
        view.backgroundColor = style.timeline.currentLineHourColor
        return view
    }()
    
    private(set) lazy var scrollView: UIScrollView = {
        let scroll = UIScrollView()
        scroll.backgroundColor = style.timeline.backgroundColor
        return scroll
    }()
    
    init(type: CalendarType, timeHourSystem: TimeHourSystem, style: Style, frame: CGRect) {
        self.type = type
        self.timeHourSystem = timeHourSystem
        self.hours = timeHourSystem.hours
        self.style = style
        super.init(frame: frame)
        
        var scrollFrame = frame
        scrollFrame.origin.y = 0
        scrollView.frame = scrollFrame
        addSubview(scrollView)
        
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(swipeEvent))
        addGestureRecognizer(panGesture)

        switch style.timeline.addEventGesture {
        case .tap:
            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(addEvent))
            addGestureRecognizer(tapGesture)
        case .longTap:
            let longGesture = UILongPressGestureRecognizer(target: self, action: #selector(addEvent))
            longGesture.minimumPressDuration = style.timeline.minimumPressDuration
            addGestureRecognizer(longGesture)
        default:
            break
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    deinit {
        stopTimer()
    }
    
    @objc private func addEvent(gesture: UITapGestureRecognizer) {
        let point = gesture.location(in: scrollView)
        let time = calculateChangeTime(pointY: point.y - style.timeline.offsetEvent - 6)
        if let minute = time.minute, let hour = time.hour {
            delegate?.didAddEvent(minute: minute, hour: hour, point: point)
        }
    }
    
    @objc private func swipeEvent(gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: self)
        let velocity = gesture.velocity(in: self)
        let endGesure = abs(translation.x) > (frame.width / 3.5)
        let events = scrollView.subviews.filter({ $0 is EventViewGeneral })
        var eventsAllDay: [UIView]
        
        if style.allDay.isPinned {
            eventsAllDay = subviews.filter({ $0 is AllDayEventView })
            eventsAllDay += subviews.filter({ $0 is AllDayTitleView })
        } else {
            eventsAllDay = scrollView.subviews.filter({ $0 is AllDayEventView })
            eventsAllDay += scrollView.subviews.filter({ $0 is AllDayTitleView })
        }

        let shadowViews = scrollView.subviews.filter { $0 is ShadowView}
        let views = events + eventsAllDay + shadowViews
        
        switch gesture.state {
        case .began, .changed:
            guard abs(velocity.y) < abs(velocity.x) else { break }
            guard endGesure else {
                delegate?.swipeX(transform: CGAffineTransform(translationX: translation.x, y: 0), stop: false)
                
                views.forEach { (view) in
                    view.transform = CGAffineTransform(translationX: translation.x, y: 0)
                }
                break
            }
    
            gesture.state = .ended
        case .failed:
            delegate?.swipeX(transform: .identity, stop: false)
            identityViews(views)
        case .cancelled, .ended:
            guard endGesure else {
                delegate?.swipeX(transform: .identity, stop: false)
                identityViews(views)
                break
            }
            
            let previousDay = translation.x > 0
            let translationX = previousDay ? frame.width : -frame.width
            
            UIView.animate(withDuration: 0.2, animations: { [weak delegate = self.delegate] in
                delegate?.swipeX(transform: CGAffineTransform(translationX: translationX * 0.8, y: 0), stop: true)
                
                views.forEach { (view) in
                    view.transform = CGAffineTransform(translationX: translationX, y: 0)
                }
            }) { [weak delegate = self.delegate] (_) in
                guard previousDay else {
                    delegate?.nextDate()
                    return
                }
                
                delegate?.previousDate()
            }
        case .possible:
            break
        @unknown default:
            fatalError()
        }
    }
    
    private func createTimesLabel(start: Int) -> [TimelineLabel] {
        var times = [TimelineLabel]()
        for (idx, hour) in hours.enumerated() where idx >= start {
            let yTime = (style.timeline.offsetTimeY + style.timeline.heightTime) * CGFloat(idx - start)
            
            let time = TimelineLabel(frame: CGRect(x: style.timeline.offsetTimeX,
                                                   y: yTime,
                                                   width: style.timeline.widthTime,
                                                   height: style.timeline.heightTime))
            time.font = style.timeline.timeFont
            time.textAlignment = .center
            time.textColor = style.timeline.timeColor
            time.text = hour
            let formatter = DateFormatter()
            formatter.dateFormat = "HH:mm"
            var hourTmp = TimeHourSystem.twentyFourHour.hours[idx]
            if idx == TimeHourSystem.twentyFourHour.hours.count - 1 {
                hourTmp += "+1d"
            }
            time.valueHash = formatter.date(from: hourTmp)?.hour.hashValue
            time.tag = idx - start
            times.append(time)
        }
        return times
    }
    
    private func createLines(times: [TimelineLabel]) -> [UIView] {
        var lines = [UIView]()
        for (idx, time) in times.enumerated() {
            let xLine = time.frame.width + style.timeline.offsetTimeX + style.timeline.offsetLineLeft
            let lineFrame = CGRect(x: xLine,
                                   y: time.center.y,
                                   width: frame.width - xLine,
                                   height: style.timeline.heightLine)
            let line = UIView(frame: lineFrame)
            if let dashPattern = style.timeline.horizontalLineDashPattern {
                line.makeDashed(color: style.timeline.horizontalLineColor, pattern: dashPattern)
            } else {
                line.backgroundColor = style.timeline.horizontalLineColor
            }
            line.tag = idx
            lines.append(line)
        }
        return lines
    }
    
    private func createVerticalLine(pointX: CGFloat) -> UIView {
        let frame = CGRect(x: pointX, y: 0, width: style.timeline.widthLine, height: (CGFloat(25) * (style.timeline.heightTime + style.timeline.offsetTimeY)) - 75)
        let line = UIView(frame: frame)
        line.tag = tagVerticalLine
        line.backgroundColor = style.timeline.verticalLineColor
        line.isHidden = !style.week.showVerticalDayDivider
        return line
    }
        
    private func createAllDayEvents(events: [Event], date: Date?, width: CGFloat, originX: CGFloat) {
        guard !events.isEmpty else { return }
        let pointY = style.allDay.isPinned ? 0 : -style.allDay.height
        let allDayEvent = AllDayEventView(events: events,
                                          frame: CGRect(x: originX, y: pointY, width: width, height: style.allDay.height),
                                          style: style.allDay,
                                          date: date)
        allDayEvent.tag = tagAllDayEvent
        allDayEvent.delegate = self
        if style.allDay.isPinned {
            addSubview(allDayEvent)
        } else {
            scrollView.addSubview(allDayEvent)
        }
        
        let allDayPlaceholder = AllDayTitleView(frame: CGRect(x: 0,
                                                              y: pointY,
                                                              width: style.timeline.widthTime + style.timeline.offsetTimeX + style.timeline.offsetLineLeft,
                                                              height: style.allDay.height),
                                                style: style.allDay)
        allDayPlaceholder.tag = tagAllDayPlaceholder
        if style.allDay.isPinned {
            addSubview(allDayPlaceholder)
        } else {
            scrollView.addSubview(allDayPlaceholder)
        }
    }
    
    private func setOffsetScrollView() {
        var offsetY: CGFloat = 0
        if !subviews.filter({ $0 is AllDayTitleView }).isEmpty || !scrollView.subviews.filter({ $0 is AllDayTitleView }).isEmpty {
            offsetY = style.allDay.height
        }
        scrollView.contentInset = UIEdgeInsets(top: offsetY, left: 0, bottom: 0, right: 0)
    }
    
    private func getTimelineLabel(hour: Int) -> TimelineLabel? {
        return scrollView.subviews .filter({ (view) -> Bool in
            guard let time = view as? TimelineLabel else { return false }
            return time.valueHash == hour.hashValue }).first as? TimelineLabel
    }
    
    private func stopTimer() {
        if timer?.isValid ?? true {
            timer?.invalidate()
            timer = nil
        }
    }
    
    private func movingCurrentLineHour() {
        guard !(timer?.isValid ?? false) else { return }
        
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            guard let self = self else { return }
            
            let nextDate = Date()
            guard self.currentTimeLabel.valueHash != nextDate.minute.hashValue else { return }
            guard let time = self.getTimelineLabel(hour: nextDate.hour) else { return }
            
            var pointY = time.frame.origin.y
            if !self.subviews.filter({ $0 is AllDayTitleView }).isEmpty, self.style.allDay.isPinned {
                pointY -= self.style.allDay.height
            }
            
            pointY = self.calculatePointYByMinute(nextDate.minute, time: time)
            
            self.currentTimeLabel.frame.origin.y = pointY - 7.5
            self.currentLineView.frame.origin.y = pointY
            self.currentTimeLabel.valueHash = nextDate.minute.hashValue
            
            let formatter = DateFormatter()
            formatter.dateFormat = self.timeHourSystem.format
            self.currentTimeLabel.text = formatter.string(from: nextDate)
        }
        
        guard let timer = timer else { return }
        RunLoop.current.add(timer, forMode: .default)
    }
    
    private func showCurrentLineHour() {
        let date = Date()
        let show: Bool = {
            switch style.timeline.currentLineHourDisplayBehaviour {
            case .showForCurrentDay:
                return selectedDate?.isOnSameDay(as: date) ?? false
            case .showAlways:
                return true
            default:
                return false
            }
        }()

        guard show, let time = getTimelineLabel(hour: date.hour) else {
            currentLineView.removeFromSuperview()
            currentTimeLabel.removeFromSuperview()
            timer?.invalidate()
            return
        }
        
        var pointY = time.frame.origin.y
        if !subviews.filter({ $0 is AllDayTitleView }).isEmpty, style.allDay.isPinned {
            pointY -= style.allDay.height
        }
        
        pointY = calculatePointYByMinute(date.minute, time: time)
        
        currentTimeLabel.frame = CGRect(x: style.timeline.offsetTimeX,
                                        y: pointY - 8,
                                        width: style.timeline.currentLineHourWidth,
                                        height: 15)
        currentLineView.frame = CGRect(x: currentTimeLabel.frame.width + style.timeline.offsetTimeX + style.timeline.offsetLineLeft,
                                       y: pointY,
                                       width: scrollView.frame.width - style.timeline.offsetTimeX,
                                       height: 1)
        
        scrollView.addSubview(currentTimeLabel)
        scrollView.addSubview(currentLineView)
        movingCurrentLineHour()
    }
    
    private func calculatePointYByMinute(_ minute: Int, time: TimelineLabel) -> CGFloat {
        let pointY: CGFloat
        if 1...59 ~= minute {
            let minutePercent = 59.0 / CGFloat(minute)
            let newY = (style.timeline.offsetTimeY + time.frame.height) / minutePercent
            let summY = (CGFloat(time.tag) * (style.timeline.offsetTimeY + time.frame.height)) + (time.frame.height / 2)
            if time.tag == 0 {
                pointY = newY + (time.frame.height / 2)
            } else {
                pointY = summY + newY
            }
        } else {
            pointY = (CGFloat(time.tag) * (style.timeline.offsetTimeY + time.frame.height)) + (time.frame.height / 2)
        }
        return pointY
    }
    
    private func identityViews(duration: TimeInterval = 0.4, delay: TimeInterval = 0.07, _ views: [UIView], action: @escaping (() -> Void) = {}) {
        UIView.animate(withDuration: duration, delay: delay, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: .curveLinear, animations: {
            views.forEach { (view) in
                view.transform = .identity
            }
            action()
        }, completion: nil)
    }

    private func performAutoScroll() {
        switch style.timeline.autoScrollBehaviour {
        case .currentTime:
            scrollToCurrentTime()
        case .firstEvent:
            _ = scrollToFirstEvent()
        case .firstEventThenCurrentTime:
            if !scrollToFirstEvent() {
                scrollToCurrentTime()
            }
        default:
            return
        }
    }

    private func scrollToCurrentTime() {
        guard !autoScrolledToCurrentTime, !autoScrolledToFirstEvent else {
            return
        }
        guard let time = getTimelineLabel(hour: Date().hour)else {
            scrollView.setContentOffset(.zero, animated: true)
            return
        }
        var frame = scrollView.frame
        frame.origin.y = time.frame.origin.y
        scrollView.scrollRectToVisible(frame, animated: true)
        autoScrolledToCurrentTime = true
    }

    private func scrollToFirstEvent() -> Bool {
        guard !autoScrolledToFirstEvent, let firstEvent = eventViews.first else {
            return false
        }
        let offset = (style.timeline.offsetTimeY)
        var frame = scrollView.frame
        frame.origin.y = firstEvent.frame.origin.y - offset
        scrollView.scrollRectToVisible(frame, animated: true)
        autoScrolledToFirstEvent = true
        return true
    }

    private func fillBackgroundDayColor(_ color: UIColor, pointX: CGFloat, width: CGFloat) -> UIView {
        let view = UIView(frame: CGRect(x: pointX, y: 0.0, width: width, height: (CGFloat(25) * (style.timeline.heightTime + style.timeline.offsetTimeY)) - 75))
        view.backgroundColor = color
        view.tag = tagBackgroundView
        return view
    }
    
    func create(dates: [Date?], events: [Event], selectedDate: Date?) {
        delegate?.didDisplayEvents(events, dates: dates)
        self.dates = dates
        self.selectedDate = selectedDate
        
        if style.allDay.isPinned {
            subviews.filter({ $0.tag == tagAllDayEvent || $0.tag == tagAllDayPlaceholder }).forEach({ $0.removeFromSuperview() })
        }
        scrollView.subviews.filter({ $0.tag != tagCurrentHourLine }).forEach({ $0.removeFromSuperview() })
        
        // filter events
        let recurringEvents = events.filter({ $0.recurringType != .none })
        let allEventsForDates = events.filter { (event) -> Bool in
            return dates.contains(where: { compareStartDate($0, with: event) || compareEndDate($0, with: event) || (checkMultipleDate($0, with: event) && type == .day) })
        }
        let filteredEvents = allEventsForDates.filter({ !$0.isAllDay })
        let filteredAllDayEvents = events.filter({ $0.isAllDay })

        // calculate a start hour
        let startHour: Int
        if !style.timeline.startFromFirstEvent {
            startHour = 0
        } else {
            if dates.count > 1 {
                startHour = filteredEvents.sorted(by: { $0.start.hour < $1.start.hour }).first?.start.hour ?? style.timeline.startHour
            } else {
                startHour = filteredEvents.filter({ compareStartDate(selectedDate, with: $0) })
                    .sorted(by: { $0.start.hour < $1.start.hour })
                    .first?.start.hour ?? style.timeline.startHour
            }
        }
        
        // add time label to timline
        let times = createTimesLabel(start: startHour)
        // add seporator line
        let lines = createLines(times: times)
        
        // calculate all height by time label
        let heightAllTimes = times.reduce(0, { $0 + ($1.frame.height + style.timeline.offsetTimeY) })
        scrollView.contentSize = CGSize(width: frame.width, height: heightAllTimes + 20)
        times.forEach({ scrollView.addSubview($0) })
        lines.forEach({ scrollView.addSubview($0) })

        let offset = style.timeline.widthTime + style.timeline.offsetTimeX + style.timeline.offsetLineLeft
        let widthPage = (frame.width - offset) / CGFloat(dates.count)
        let heightPage = (CGFloat(times.count) * (style.timeline.heightTime + style.timeline.offsetTimeY)) - 75
        
        // horror
        for (idx, date) in dates.enumerated() {
            let pointX: CGFloat
            if idx == 0 {
                pointX = offset
            } else {
                pointX = CGFloat(idx) * widthPage + offset
            }
            scrollView.addSubview(createVerticalLine(pointX: pointX))
            
            let eventsByDate = filteredEvents.filter({ compareStartDate(date, with: $0) || compareEndDate(date, with: $0) || checkMultipleDate(date, with: $0) })
            let allDayEvents = filteredAllDayEvents.filter({ compareStartDate(date, with: $0) || compareEndDate(date, with: $0) })
            
            let recurringEventByDate: [Event]
            if !recurringEvents.isEmpty {
                recurringEventByDate = recurringEvents.reduce([], { (acc, event) -> [Event] in
                    guard !eventsByDate.contains(where: { $0.ID == event.ID }) else { return acc }
                    
                    guard let recurringEvent = event.updateDate(newDate: date, calendar: style.calendar) else {
                        return acc
                    }
                    
                    return acc + [recurringEvent]
                    
                })
            } else {
                recurringEventByDate = []
            }
            
            let filteredRecurringEvents = recurringEventByDate.filter({ !$0.isAllDay })
            let filteredAllDayRecurringEvents = recurringEventByDate.filter({ $0.isAllDay })
            let sortedEventsByDate = (eventsByDate + filteredRecurringEvents).sorted(by: { $0.start < $1.start })
            
            // create an all day events
            createAllDayEvents(events: allDayEvents + filteredAllDayRecurringEvents, date: date, width: widthPage, originX: pointX)
            
            // get layout descriptors
            let layoutDescriptors = createLayoutDescriptors(from: eventsByDate)
            var pagesCached = [EventViewGeneral]()
            
            if !sortedEventsByDate.isEmpty {
                // create event
                var newFrame = CGRect(x: 0, y: 0, width: 0, height: heightPage)
                sortedEventsByDate.forEach { (event) in
                    times.forEach({ (time) in                        
                        // calculate position 'y'
                        if event.start.hour.hashValue == time.valueHash, event.start.day == date?.day {
                            newFrame.origin.y = calculatePointYByMinute(event.start.minute, time: time)
                        } else if let firstTimeLabel = getTimelineLabel(hour: startHour), event.start.day != date?.day {
                            newFrame.origin.y = calculatePointYByMinute(startHour, time: firstTimeLabel)
                        }
                        
                        // calculate 'height' event
                        if event.end.hour.hashValue == time.valueHash, event.end.day == date?.day {
                            let summHeight = (CGFloat(time.tag) * (style.timeline.offsetTimeY + time.frame.height)) - newFrame.origin.y + (time.frame.height / 2)
                            if 0..<60 ~= event.end.minute {
                                let minutePercent = 60.0 / CGFloat(event.end.minute)
                                let newY = (style.timeline.offsetTimeY + time.frame.height) / minutePercent
                                newFrame.size.height = summHeight + newY - style.timeline.offsetEvent
                            } else {
                                newFrame.size.height = summHeight - style.timeline.offsetEvent
                            }
                        } else if event.end.day != date?.day {
                            newFrame.size.height = (CGFloat(time.tag) * (style.timeline.offsetTimeY + time.frame.height)) - newFrame.origin.y + (time.frame.height / 2)
                        }
                    })
                    
                    // calculate 'width' and position 'x'
                    var newWidth = widthPage
                    var newPointX = pointX
                    if let layoutDescriptor = layoutDescriptors.first(where: { $0.id == event.ID }) {
                        newWidth *= layoutDescriptor.widthPercetage
                        newWidth -= style.timeline.offsetEvent
                        newFrame.size.width = newWidth
                        
                        if layoutDescriptor.intesectingEvents.count > 0, !pagesCached.isEmpty {
                            for page in pagesCached {
                                while page.frame.intersects(CGRect(x: newPointX, y: newFrame.origin.y, width: newFrame.width, height: newFrame.height)) {
                                    newPointX += (page.frame.width + style.timeline.offsetEvent).rounded()
                                }
                            }
                        }
                    }
                    
                    newFrame.origin.x = newPointX
                    
                    let page: EventViewGeneral
                    if let pageView = dataSource?.willDisplayEventView(event, frame: newFrame, date: date) {
                        page = pageView
                    } else {
                        page = EventView(event: event, style: style, frame: newFrame)
                    }

                    if let shadowView = createShadowView(with: style, event: event, frame: newFrame) {
                        scrollView.addSubview(shadowView)
                    }
                    page.delegate = self
                    page.dataSource = self
                    scrollView.addSubview(page)

                    pagesCached.append(page)
                }
            }
        }
        setOffsetScrollView()
        performAutoScroll()
        showCurrentLineHour()
    }
}

private extension TimelineView {

    class ShadowView: UIView {
        init(frame: CGRect, style: Style) {
            super.init(frame: frame)
            setup(with: style)
        }

        override init(frame: CGRect) {
            fatalError("Not supported")
        }

        required init?(coder: NSCoder) {
            fatalError("Not supported")
        }

        private func setup(with style: Style) {
            backgroundColor = .clear
            layer.shadowOpacity = style.timeline.eventShadowOpacity
            layer.shadowColor = style.timeline.eventShadowColor.cgColor
            layer.shadowOffset = style.timeline.eventShadowOffset
            layer.shadowRadius = style.timeline.eventShadowRadius
            layer.shadowPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: frame.size), cornerRadius: style.timeline.eventCornersRadius.width).cgPath
            layer.masksToBounds = false
        }
    }

    func createShadowView(with style: Style, event: Event, frame: CGRect) -> UIView? {
        guard event.hasShadow, style.timeline.eventShadowColor != .clear, style.timeline.eventShadowOpacity > 0 else {
            return nil
        }
        return ShadowView(frame: frame, style: style)
    }
}

extension UIView {
    func makeDashed(color: UIColor, pattern: [NSNumber]) {
        let yMid = self.bounds.midY
        let xMax = self.bounds.maxX
        let pointFrom = CGPoint(x: 0, y: yMid)
        let pointTo = CGPoint(x: xMax, y: yMid)

        let shapeLayer = CAShapeLayer()
        shapeLayer.strokeColor = color.cgColor
        shapeLayer.lineWidth = self.frame.height
        shapeLayer.lineDashPattern = pattern
        let path = CGMutablePath()
        path.addLines(between: [pointFrom, pointTo])
        shapeLayer.path = path
        layer.addSublayer(shapeLayer)
    }
}

extension TimelineView {

    var eventViews: [EventViewGeneral] {
        return scrollView.subviews.compactMap{ $0 as? EventViewGeneral }
    }
    var allDayViews: [AllDayEventView] {
        if style.allDay.isPinned {
            return subviews.compactMap { $0 as? AllDayEventView }
        } else {
            return scrollView.subviews.compactMap { $0 as? AllDayEventView }
        }
    }
}
