package persistence

import (
	"fmt"
	"regexp"
	"strconv"
	"time"
)

var yearRE = regexp.MustCompile(`^(\d{4})$`)
var monthRE = regexp.MustCompile(`^(\d{4})-(\d{2})$`)
var weekRE = regexp.MustCompile(`^(\d{4})-W(\d{2})$`)
var dayRE = regexp.MustCompile(`^(\d{4})-(\d{2})-(\d{2})$`)
var hourRE = regexp.MustCompile(`^(\d{4})-(\d{2})-(\d{2})T(\d{2})$`)

type Granularity int

const (
	Year Granularity = iota
	Month
	Week
	Day
	Hour
)

// ParseISO8601 is **not** a function to parse all and every kind of valid ISO 8601
// date, nor it's intended to be, since we don't need that.
func ParseISO8601(s string) (*time.Time, Granularity, error) {
	if matches := yearRE.FindStringSubmatch(s); len(matches) != 0 {
		year, err := parseYear(matches[1])
		if err != nil {
			return nil, -1, err
		}

		t := time.Date(year, time.December, daysOfMonth(time.December, year), 23, 59, 59, 0, time.UTC)
		return &t, Year, nil
	}

	if matches := monthRE.FindStringSubmatch(s); len(matches) != 0 {
		month, err := parseMonth(matches[2])
		if err != nil {
			return nil, -1, err
		}
		year, err := parseYear(matches[1])
		if err != nil {
			return nil, -1, err
		}

		t := time.Date(year, month, 31, 23, 59, 59, 0, time.UTC)
		return &t, Month, nil
	}

	if matches := weekRE.FindStringSubmatch(s); len(matches) != 0 {
		week, err := parseWeek(matches[2])
		if err != nil {
			return nil, -1, err
		}
		year, err := parseYear(matches[1])
		if err != nil {
			return nil, -1, err
		}

		t := time.Date(year, time.January, week*7, 23, 59, 59, 0, time.UTC)
		return &t, Week, nil
	}

	if matches := dayRE.FindStringSubmatch(s); len(matches) != 0 {
		month, err := parseMonth(matches[2])
		if err != nil {
			return nil, -1, err
		}
		year, err := parseYear(matches[1])
		if err != nil {
			return nil, -1, err
		}
		day, err := parseDay(matches[3], daysOfMonth(month, year))
		if err != nil {
			return nil, -1, err
		}

		t := time.Date(year, month, day, 23, 59, 59, 0, time.UTC)
		return &t, Day, nil
	}

	if matches := hourRE.FindStringSubmatch(s); len(matches) != 0 {
		month, err := parseMonth(matches[2])
		if err != nil {
			return nil, -1, err
		}
		year, err := parseYear(matches[1])
		if err != nil {
			return nil, -1, err
		}
		hour, err := parseHour(matches[4])
		if err != nil {
			return nil, -1, err
		}
		day, err := parseDay(matches[3], daysOfMonth(month, year))
		if err != nil {
			return nil, -1, err
		}

		t := time.Date(year, month, day, hour, 59, 59, 0, time.UTC)
		return &t, Hour, nil
	}

	return nil, -1, fmt.Errorf("string does not match any formats")
}

func daysOfMonth(month time.Month, year int) int {
	switch month {
	case time.January:
		return 31
	case time.February:
		if isLeap(year) {
			return 29
		} else {
			return 28
		}
	case time.March:
		return 31
	case time.April:
		return 30
	case time.May:
		return 31
	case time.June:
		return 30
	case time.July:
		return 31
	case time.August:
		return 31
	case time.September:
		return 30
	case time.October:
		return 31
	case time.November:
		return 30
	case time.December:
		return 31
	default:
		panic("invalid month!")
	}
}

func isLeap(year int) bool {
	if year%4 != 0 {
		return false
	} else if year%100 != 0 {
		return true
	} else if year%400 != 0 {
		return false
	} else {
		return true
	}
}

func atoi(s string) int {
	i, e := strconv.Atoi(s)
	if e != nil {
		// panic on error since atoi() will be called only after we parse it with regex
		// (hopefully `\d`!)
		panic(e.Error())
	}
	return i
}

func parseYear(s string) (int, error) {
	year := atoi(s)
	if year <= 1583 {
		return 0, fmt.Errorf("years before 1583 are not allowed")
	}
	return year, nil
}

func parseMonth(s string) (time.Month, error) {
	month := atoi(s)
	if month <= 0 || month >= 13 {
		return time.Month(-1), fmt.Errorf("month is not in range [01, 12]")
	}
	return time.Month(month), nil
}

func parseWeek(s string) (int, error) {
	week := atoi(s)
	if week <= 0 || week >= 54 {
		return -1, fmt.Errorf("week is not in range [01, 53]")
	}
	return week, nil
}

func parseDay(s string, max int) (int, error) {
	day := atoi(s)
	if day <= 0 || day > max {
		return -1, fmt.Errorf("day is not in range [01, %d]", max)
	}
	return day, nil
}

func parseHour(s string) (int, error) {
	hour := atoi(s)
	if hour <= -1 || hour >= 25 {
		return -1, fmt.Errorf("hour is not in range [00, 24]")
	}
	return hour, nil
}
