// Copyright 2022 Brian J. Downs
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package openweathermap

import (
	"fmt"
	"io"
	"net/url"
	"strconv"
	"strings"
)

// ForecastSys area population
type ForecastSys struct {
	Population int `json:"population"`
}

// Temperature holds returned termperate sure stats
type Temperature struct {
	Day   float64 `json:"day"`
	Min   float64 `json:"min"`
	Max   float64 `json:"max"`
	Night float64 `json:"night"`
	Eve   float64 `json:"eve"`
	Morn  float64 `json:"morn"`
}

// City data for given location
type City struct {
	ID         int         `json:"id"`
	Name       string      `json:"name"`
	Coord      Coordinates `json:"coord"`
	Country    string      `json:"country"`
	Population int         `json:"population"`
	Sys        ForecastSys `json:"sys"`
}

type ForecastWeather interface {
	DailyByName(location string, days int) error
	DailyByCoordinates(location *Coordinates, days int) error
	DailyByID(id, days int) error
}

// json served by OWM API can take different forms, so all of them must be matched
// by corresponding data type and unmarshall method
type ForecastWeatherJson interface {
	Decode(r io.Reader) error
}

type ForecastWeatherData struct {
	Unit    string
	Lang    string
	Key     string
	baseURL string
	*Settings
	ForecastWeatherJson
}

// NewForecast returns a new HistoricalWeatherData pointer with
// the supplied arguments.
func NewForecast(forecastType, unit, lang, key string, options ...Option) (*ForecastWeatherData, error) {
	unitChoice := strings.ToUpper(unit)
	langChoice := strings.ToUpper(lang)

	if forecastType != "16" && forecastType != "5" {
		return nil, errForecastUnavailable
	}

	if !ValidDataUnit(unitChoice) {
		return nil, errUnitUnavailable
	}

	if !ValidLangCode(langChoice) {
		return nil, errLangUnavailable
	}

	settings := NewSettings()
	if err := setOptions(settings, options); err != nil {
		return nil, err
	}

	var err error
	k, err := setKey(key)
	if err != nil {
		return nil, err
	}
	forecastData := ForecastWeatherData{
		Unit:     DataUnits[unitChoice],
		Lang:     langChoice,
		Key:      k,
		Settings: settings,
	}

	if forecastType == "16" {
		forecastData.baseURL = forecast16Base
		forecastData.ForecastWeatherJson = &Forecast16WeatherData{}
	} else {
		forecastData.baseURL = forecast5Base
		forecastData.ForecastWeatherJson = &Forecast5WeatherData{}
	}

	return &forecastData, nil
}

// DailyByName will provide a forecast for the location given for the
// number of days given.
func (f *ForecastWeatherData) DailyByName(location string, days int) error {
	response, err := f.client.Get(fmt.Sprintf(f.baseURL, f.Key, fmt.Sprintf("%s=%s", "q", url.QueryEscape(location)), f.Unit, f.Lang, days))
	if err != nil {
		return err
	}
	defer response.Body.Close()

	return f.ForecastWeatherJson.Decode(response.Body)
}

// DailyByCoordinates will provide a forecast for the coordinates ID give
// for the number of days given.
func (f *ForecastWeatherData) DailyByCoordinates(location *Coordinates, days int) error {
	response, err := f.client.Get(fmt.Sprintf(f.baseURL, f.Key, fmt.Sprintf("lat=%f&lon=%f", location.Latitude, location.Longitude), f.Unit, f.Lang, days))
	if err != nil {
		return err
	}
	defer response.Body.Close()

	return f.ForecastWeatherJson.Decode(response.Body)
}

// DailyByID will provide a forecast for the location ID give for the
// number of days given.
func (f *ForecastWeatherData) DailyByID(id, days int) error {
	response, err := f.client.Get(fmt.Sprintf(f.baseURL, f.Key, fmt.Sprintf("%s=%s", "id", strconv.Itoa(id)), f.Unit, f.Lang, days))
	if err != nil {
		return err
	}
	defer response.Body.Close()

	return f.ForecastWeatherJson.Decode(response.Body)
}

// DailyByZip will provide a forecast for the provided zip code.
//
// Deprecated: use DailyByZipcode instead.
func (f *ForecastWeatherData) DailyByZip(zip int, countryCode string, days int) error {
	response, err := f.client.Get(fmt.Sprintf(f.baseURL, f.Key, fmt.Sprintf("zip=%05d,%s", zip, countryCode), f.Unit, f.Lang, days))
	if err != nil {
		return err
	}
	defer response.Body.Close()

	return f.ForecastWeatherJson.Decode(response.Body)
}

// DailyByZipcode will provide a forecast for the provided zip code.
func (f *ForecastWeatherData) DailyByZipcode(zip string, countryCode string, days int) error {
	response, err := f.client.Get(fmt.Sprintf(f.baseURL, f.Key, fmt.Sprintf("zip=%s,%s", zip, countryCode), f.Unit, f.Lang, days))
	if err != nil {
		return err
	}
	defer response.Body.Close()

	return f.ForecastWeatherJson.Decode(response.Body)
}
