import { table } from "table";

export const CONSTANT_DAYS_ORDER = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

export interface CalendarOptions {
    year?: number;
    month?: number;
}

export interface CalendarDaysData {
    row: number;
    day?: number;
    name?: string;
    holiday: boolean;
}

export interface CalendarData {
    year: number;
    month: number;
    totalDays: number;
    monthName: string;
    days: CalendarDaysData[];
    leapYear: boolean;
    leapMonth: boolean;
}

export interface createHTMLOptions {
    includeStyles?: boolean;
}

class Calendar {
    year: number;
    month: number;
    days: string[];
    months: string[];
    holidays: number[];

    /**
     * Creates new Calendar
     * @example ```js
     * const calendar = new Calendar({ year: 2021, month: 6 });
     * console.log(calendar.create());
     * ```
     */
    constructor(options?: CalendarOptions) {

        /**
         * Current year
         */
        this.year = this._validate(options && typeof options.year === "number" ? options.year : new Date().getFullYear(), "number", "year");

        /**
         * Current month
         */
        this.month = this._validate(options && typeof options.month === "number" ? options.month : new Date().getMonth(), "number", "month");

        /**
         * Days format
         */
        this.days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

        /**
         * Months format
         */
        this.months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];

        /**
         * Holiday(s). By default, it is set to sunday
         */
        this.holidays = [0];
    }

    /**
     * Sets custom days notation
     * @param days Days name
     */
    public setDays(days: string[]): Calendar {
        const isValid = days.length === 7 && days.some(x => typeof x === "string");
        if (!isValid) throw new Error("Invalid days data");

        this.days = days;
        return this;
    }

    /**
     * Sets custom months notaiton
     * @param months Months name
     */
    public setMonths(months: string[]): Calendar {
        const isValid = months.length === 12 && months.some(x => typeof x === "string");
        if (!isValid) throw new Error("Invalid months data");

        this.months = months;
        return this;
    }

    /**
     * Sets holidays
     * @param data Holidays day array. Example: `[0]` refers to `sunday` whereas `[0, 6]` refers to `saturday` and `sunday`.
     */
    public setHolidays(data: number[]) {
        const isValid = data.some(x => typeof x === "number" && (x >= 0 && x < 7));
        if (!isValid) throw new Error("Invalid holidays data");

        this.holidays = data;
        return this;
    }

    /**
     * Returns total number of days in this month
     */
    public get length(): number {
        if (this.month === 1 && ((this.year % 4 == 0 && this.year % 100 !== 0) || this.year % 400 === 0)) return 29;
        return CONSTANT_DAYS_ORDER[this.month]
    }

    /**
     * If it is a leap year
     */
    public get leap(): boolean {
        return (this.year % 4 == 0 && this.year % 100 !== 0) || this.year % 400 === 0;
    }

    /**
     * Returns current month name
     */
    public get monthName(): string {
        return this.months[this.month];
    }

    /**
     * Creates calendar
     * @example ```js
     * const data = new Calendar({ year: 2020, month: 0 });
     * console.log(data.create());
     * ```
     */
    public create(): CalendarData {
        const firstDay = new Date(this.year, this.month, 1);
        const intitalDay = firstDay.getDay();
        const length = this.length;
        const month = this.monthName;
        
        let days = [];

        let day = 1;

        for (let i = 0; i < 9; i++) {
            for (let j = 0; j < 7; j++) {
                if (day <= length && (i > 0 || j >= intitalDay)) {
                    days.push({
                        row: i,
                        day: day,
                        name: this.days[j],
                        holiday: this.holidays.some(x => x === j)
                    });
                    day++;
                } else {
                    days.push({
                        row: i,
                        day: null,
                        name: null,
                        holiday: false
                    })
                }
            }

            if (day > length) break;
        }

        return {
            year: this.year,
            month: this.month,
            totalDays: length,
            monthName: month,
            days: days,
            leapMonth: this.length === 29,
            leapYear: this.leap
        } as CalendarData;
    }

    /**
     * Creates HTML output
     * @param options Calendar html output options
     */
    public toHTML(options?: createHTMLOptions): string {
        if (!options) options = {};
        const data = this.create();
        const cap = `<caption>${data.monthName} ${data.year}</caption>`;
        let heading = "";
        let days = "";
        const ROWS = [...new Set(data.days.map(m => m.row))];

        for (let i = 0; i < 7; i++) {
            heading += `<th class="${this.days[i].toLowerCase()}">${this.days[i]}</th>`;
        }

        ROWS.forEach(index => {
            let tempr = "";
            const row = data.days.filter(x => x.row === index);
            for (let i = 0; i < row.length; i++) {
                tempr += `<td class="day${row[i].holiday ? " holiday" : ""}">${row[i].day || ""}</td>`;
            }
            days += `<tr>${tempr}</tr>`;
        });

        const style = `
        <style>
            table,
            th,
            td {
                border: 1px solid black;
                border-collapse: collapse;
            }
        
            th,
            td {
                padding: 5px;
                text-align: left;
            }

            .holiday {
                color: #db0000;
            }
        </style>
        `;

        return `${!!options.includeStyles ? style : ""}<table width="100%">${cap}<thead><tr>${heading}</tr></thead><tbody>${days}</tbody></table>`;
    }

    /**
     * String representation of the calendar
     */
    public toString(): string {
        const data = this.create();
        const ROWS = [...new Set(data.days.map(m => m.row))];
        const heading: string[] = [];
        const days: any[] = [];

        for (let i = 0; i < 7; i++) {
            heading.push(this.days[i]);
        }

        ROWS.forEach(index => {
            const row = data.days.filter(x => x.row === index).map(m => `${m.day || ""}`);
            days.push(row);
        });

        const header = table([[data.monthName, data.year]]);

        const body = [
            heading,
            ...days
        ];

        const tableData = table(body, {
            border: {
                topBody: "─",
                topJoin: "┬",
                topLeft: "┌",
                topRight: "┐",

                bottomBody: "─",
                bottomJoin: "┴",
                bottomLeft: "└",
                bottomRight: "┘",

                bodyLeft: "│",
                bodyRight: "│",
                bodyJoin: "│",

                joinBody: "─",
                joinLeft: "├",
                joinRight: "┤",
                joinJoin: "┼"
            }
        });

        return `${header}${tableData}`;
    }

    toJSON(): CalendarData {
        return this.create();
    }

    /**
     * Static calendar creator
     * @param year Creates calendar of full year
     * @example ```js
     * const calendar = Calendar.createCalendar(2020);
     * console.log(calendar.map(m => m.monthName));
     * ```
     */
    public static createCalendar(year = new Date().getFullYear()): CalendarData[] {
        const valid = typeof year === "number" && year >= 1000 && year <= 9999;
        if (!valid) throw new TypeError(`Invalid year: ${year}!`);

        const cal = [];
        for (let i = 0; i < 12; i++) {
            const st = new Calendar({
                year: year,
                month: i
            });

            cal.push(st.create());
        }

        return cal;
    }

    /**
     * Internal number validation method
     * @param data data to validate
     * @param vtype value data type
     * @param dataType validation data type
     */
    private _validate(data: number, vtype = "number", dataType: "year" | "month"): number {
        
        switch(dataType) {
            case "month":
                if (typeof data === vtype && !(data < 0 && data > 11)) return data;
                throw new TypeError(`Invalid month: ${data}!`);
            case "year":
                if (typeof data === vtype && !(data < 1000 && data > 9999)) return data;
                throw new TypeError(`Invalid year: ${data}!`);
            default:
                throw new Error("Invalid type");
        }

    }

}

export { Calendar };
export default Calendar;