//
//  ForecastView.swift
//  1Weather
//
//  Created by Dmitry Stepanets on 11.03.2021.
//

import UIKit

private struct DailyGraphPoints {
    let maxTempPoints: [CGPoint]
    let minTempPoints: [CGPoint]
}

private struct HourlyGraphPoints {
    let points: [CGPoint]
}

public enum ForecastType {
    case daily
    case hourly
    case wind
}

protocol ForecastTimePeriodViewDelegate:class {
    func forecastTimePeriodView(view:ForecastTimePeriodView, didSelectButtonAt index:Int)
    func offsetDidChange(offset:CGFloat)
}

class ForecastTimePeriodView: UIView {
    //Private
    private let scrollView = UIScrollView()
    private let stackView = UIStackView()
    private let graphView = GraphView()
    private var graphRect:CGRect = .zero
    private var currentForecastType = ForecastType.daily
    private var dailyGraphPoints = DailyGraphPoints(maxTempPoints: [CGPoint](), minTempPoints: [CGPoint]())
    private var hourlyGraphPoints = HourlyGraphPoints(points: [CGPoint]())
    private var daily = [DailyWeather]()
    private var hourly = [HourlyWeather]()
    
    //Public
    weak var delegate:ForecastTimePeriodViewDelegate?
    var isEmpty:Bool {
        return dailyGraphPoints.maxTempPoints.isEmpty && hourlyGraphPoints.points.isEmpty
    }
    
    //MARK:- View life cycle
    init() {
        super.init(frame: .zero)
        
        preapreView()
        prepareScrollView()
        prepareStackView()
        prepareGraphView()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    //Public
    public func set(daily:[DailyWeather]? = nil, hourly:[HourlyWeather]? = nil) {
        if let inputDaily = daily {
            self.daily = inputDaily
        }
        
        if let inputHourly = hourly {
            self.hourly = inputHourly
        }
    }
    
    public func set(forecastType:ForecastType, buttonType: PeriodButtonProtocol.Type) {
        self.currentForecastType = forecastType
        rebuildButtons(buttonType: buttonType)
    }
    
    public func selectButtonAt(index:Int) {
        guard let buttons = self.stackView.arrangedSubviews as? [PeriodButtonProtocol] else {
            return
        }
        
        buttons.enumerated().forEach {
            $1.isSelected = $0 == index
        }
    }
    
    public func update(offset:CGFloat) {
        if self.scrollView.contentOffset.x != offset {
            self.scrollView.setContentOffset(.init(x: offset, y: 0), animated: false)
        }
    }
    
    //Private
    @objc private func handleForecastButton(button:UIControl) {
        stackView.arrangedSubviews.enumerated().forEach {
            if let periodButton = $1 as? PeriodButtonProtocol {
                periodButton.isSelected = periodButton === button
                
                if periodButton.isSelected {
                    tintGraphAt(button: periodButton)
                    delegate?.forecastTimePeriodView(view: self, didSelectButtonAt: $0)
                }
            }
        }
    }
    
    private func rebuildButtons(buttonType: PeriodButtonProtocol.Type) {
        stackView.removeAll()
        
        let numberOfButtons:Int
        switch currentForecastType {
        case .daily:
            numberOfButtons = daily.count
        case .hourly, .wind:
            numberOfButtons = hourly.count
        }
        
        for index in 0..<numberOfButtons {
            let forecastButton = buttonType.init()
            switch currentForecastType {
            case .daily:
                forecastButton.configure(dailyWeather: daily[index])
            case .hourly, .wind:
                forecastButton.configure(hourlyWeather: hourly[index])
            }
            forecastButton.index = index
            forecastButton.addTarget(self, action: #selector(handleForecastButton(button:)), for: .touchUpInside)
            forecastButton.isSelected = index == 0
            stackView.addArrangedSubview(forecastButton)
            
            forecastButton.snp.makeConstraints { (make) in
                make.height.equalToSuperview()
            }
        }
        
        stackView.layoutIfNeeded()
        updateGraphLayout()
    }
    
    private func updateGraphLayout() {
        print("[ForecastTimePeriod] Update graph layout")
        graphRect = (stackView.arrangedSubviews.first as? PeriodButtonProtocol)?.graphRect ?? .zero
        graphView.frame = .init(x: 0,
                                y: graphRect.origin.y,
                                width: stackView.frame.width,
                                height: graphRect.height)
        
        updateGraphPoints()
        drawGraph()
    }
    
    private func updateGraphPoints() {
        switch currentForecastType {
        case .daily:
            updateDailyGraphPoints()
        case .hourly:
            updateHourlyGraphPoints()
        case .wind:
            updateWindPoints()
        }
    }
    
    private func updateDailyGraphPoints() {
        let daysCount = daily.count
        let maxTemps = (daily.map{ CGFloat($0.maxTemp?.localizedValue ?? 0) })
        let topMaxTemp = maxTemps.max() ?? 0
        let minTemps = (daily.map{ CGFloat($0.minTemp?.localizedValue ?? 0) })
        let topMinTemp = minTemps.max() ?? 0
       
        var maxPoints = [CGPoint]()
        var minPoints = [CGPoint]()
        for index in 0..<daysCount {
            guard let stackButton = stackView.arrangedSubviews[index] as? PeriodButtonProtocol else { continue }
            
            let buttonRightSide = stackButton.frame.origin.x + stackButton.bounds.width
            let buttonCenterX = (buttonRightSide + stackButton.frame.origin.x) / 2
            
            let maxTempLevelsCount = CGFloat(Set(maxTemps).count)
            let minTempLevelsCount = CGFloat(Set(minTemps).count)
            let totalLevels = maxTempLevelsCount + minTempLevelsCount
            let levelHeight = (graphView.frame.height / CGFloat(totalLevels)).rounded(.down)
            
            let maxTempFrame = CGRect(x: 0, y: 0, width: graphView.frame.width, height: maxTempLevelsCount * levelHeight)
            let minTempFrame = CGRect(x: 0, y: maxTempFrame.height, width: graphView.frame.width, height: minTempLevelsCount * levelHeight)
            
            //Max
            var maxPointLevel = maxTempFrame.origin.y + ((topMaxTemp - maxTemps[index]) * levelHeight) + levelHeight
            maxPointLevel = min(maxPointLevel, maxTempFrame.height)
            
            //Min
            var minPointLevel = minTempFrame.origin.y + ((topMinTemp - minTemps[index]) * levelHeight) + levelHeight
            minPointLevel = min(minPointLevel, minTempFrame.height + minTempFrame.origin.y)
            
            maxPoints.append(.init(x: buttonCenterX, y: maxPointLevel))
            minPoints.append(.init(x: buttonCenterX, y: minPointLevel))
        }
        
        dailyGraphPoints = DailyGraphPoints(maxTempPoints: maxPoints, minTempPoints: minPoints)
    }
    
    private func updateHourlyGraphPoints() {
        let hoursCount = hourly.count
        let temps = (hourly.map{ CGFloat($0.temp?.localizedValue ?? 0) })
        let maxTemp = temps.max() ?? 0
       
        var points = [CGPoint]()
        for index in 0..<hoursCount {
            guard let stackButton = stackView.arrangedSubviews[index] as? PeriodButtonProtocol else { continue }
            
            let buttonRightSide = stackButton.frame.origin.x + stackButton.bounds.width
            let buttonCenterX = (buttonRightSide + stackButton.frame.origin.x) / 2
            
            let levelsCount = CGFloat(Set(temps).count)
            let levelHeight = (graphView.frame.height / CGFloat(levelsCount)).rounded(.down)
            let tempFrame = CGRect(x: 0, y: 0, width: graphView.frame.width, height: graphView.frame.height)
            
            var pointLevel = tempFrame.origin.y + ((maxTemp - temps[index]) * levelHeight)
            pointLevel = max(pointLevel, tempFrame.origin.y)
            pointLevel = min(tempFrame.height, pointLevel)
         
            points.append(.init(x: buttonCenterX, y: pointLevel))
        }
        
        hourlyGraphPoints = HourlyGraphPoints(points: points)
    }
    
    private func updateWindPoints() {
        let hoursCount = hourly.count
        let speeds = (hourly.map{ CGFloat($0.windSpeed?.value ?? 0) })
        let maxWindSpeed = speeds.max() ?? 0
        
        var points = [CGPoint]()
        for index in 0..<hoursCount {
            guard let stackButton = stackView.arrangedSubviews[index] as? PeriodButtonProtocol else { continue }
            
            let buttonRightSide = stackButton.frame.origin.x + stackButton.bounds.width
            let buttonCenterX = (buttonRightSide + stackButton.frame.origin.x) / 2
            
            let levelsCount = CGFloat(Set(speeds).count)
            let levelHeight = (graphView.frame.height / CGFloat(levelsCount)).rounded(.down)
            let graphFrame = CGRect(x: 0, y: 0, width: graphView.frame.width, height: graphView.frame.height - 5)
            
            var pointLevel = graphFrame.origin.y + ((maxWindSpeed - speeds[index]) * levelHeight)
            pointLevel = max(pointLevel, graphFrame.origin.y)
            pointLevel = min(graphFrame.height, pointLevel)
         
            points.append(.init(x: buttonCenterX, y: pointLevel))
        }
        
        hourlyGraphPoints = HourlyGraphPoints(points: points)
    }
    
    private func drawGraph() {
        guard
            let periodButtons = stackView.arrangedSubviews as? [PeriodButtonProtocol],
            let selectedButton = (periodButtons.first{ $0.isSelected })
        else {
            return
        }
        
        switch currentForecastType {
        case .daily:
            self.graphView.drawMainGraph(with: dailyGraphPoints.maxTempPoints)
            self.graphView.drawAdditionalGraph(with: dailyGraphPoints.minTempPoints)
            self.tintGraphAt(button: selectedButton)
        case .hourly:
            self.graphView.drawMainGraph(with: hourlyGraphPoints.points)
            self.graphView.drawAdditionalGraph(with: [CGPoint]())
            self.tintGraphAt(button: selectedButton)
        case .wind:
            self.graphView.drawWindGraph(with: hourlyGraphPoints.points)
            self.graphView.drawAdditionalGraph(with: [CGPoint]())
            self.tintGraphAt(button: selectedButton)
        }
        
        print("[ForecastTimePeriod] Draw graph")
    }
    
    private func tintGraphAt(button:PeriodButtonProtocol) {
        guard button.tinted else { return }
        
        switch currentForecastType {
        case .daily:
            self.graphView.tintGraphFrom(startPointX: button.frame.origin.x,
                                         endPointX: button.frame.origin.x + button.bounds.width)
            self.graphView.tintMainDotAt(point: dailyGraphPoints.maxTempPoints[button.index])
            self.graphView.tintAdditionalDotAt(point: dailyGraphPoints.minTempPoints[button.index])
        case .hourly, .wind:
            self.graphView.tintGraphFrom(startPointX: button.frame.origin.x,
                                         endPointX: button.frame.origin.x + button.bounds.width)
            self.graphView.tintMainDotAt(point: hourlyGraphPoints.points[button.index])
        }
    }
}

//MARK:- Prepare
private extension ForecastTimePeriodView {
    func preapreView() {
        backgroundColor = .clear
    }
    
    func prepareScrollView() {
        scrollView.delegate = self
        scrollView.showsVerticalScrollIndicator = false
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.clipsToBounds = false
        addSubview(scrollView)
        
        scrollView.snp.makeConstraints { (make) in
            make.edges.equalToSuperview()
        }
    }
    
    func prepareStackView() {
        stackView.axis = .horizontal
        stackView.distribution = .equalSpacing
        stackView.alignment = .center
        stackView.spacing = 10
        stackView.clipsToBounds = false
        stackView.isLayoutMarginsRelativeArrangement = true
        stackView.layoutMargins = .init(top: 0, left: 6, bottom: 0, right: 6)
        scrollView.addSubview(stackView)
        
        stackView.snp.makeConstraints { (make) in
            make.edges.height.equalToSuperview()
        }
    }
    
    func prepareGraphView() {
        //Graph view
        graphView.frame = .zero
        graphView.backgroundColor = .clear
        scrollView.addSubview(graphView)
    }
}

//MARK:- UIScrollView Delegate
extension ForecastTimePeriodView: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        self.delegate?.offsetDidChange(offset: scrollView.contentOffset.x)
    }
}
