﻿import * as Objects from "../objects";
import * as Utils from "../utils";
import * as Constants from "../constants";
import { LayoutContract } from "../layouts/layoutContract";
import { IObjectStorage, Query, Operator, Page } from "../persistence";
import { ILayoutService, LayoutMetadata, LayoutLocalizedContract } from "./";
import { Contract } from "..";
import { layoutTemplate } from "./layoutTemplate";
import { ILocaleService } from "../localization";
import { Logger } from "../logging";

const documentsPath = "files";

export interface PermalinkMatchToken {
    index: number;
    name: string;
    value?: string;
}

export interface PermalinkMatchResult {
    match: boolean;
    tokens: PermalinkMatchToken[];
}

export class LayoutService implements ILayoutService {
    protected layoutsPath: string = "layouts";

    constructor(
        private readonly objectStorage: IObjectStorage,
        private readonly localeService: ILocaleService,
        private readonly logger: Logger
    ) { }

    /**
     * Copies limited number of metadata properties.
     */
    private copyMetadata(sourceMetadata: LayoutMetadata, targetMetadata: LayoutMetadata): LayoutMetadata {
        if (!sourceMetadata) {
            throw new Error(`Parameter "sourceMetadata" not specified.`);
        }

        if (!targetMetadata) {
            throw new Error(`Parameter "targetMetadata" not specified.`);
        }

        targetMetadata.title = sourceMetadata.title;
        targetMetadata.description = sourceMetadata.description;
        targetMetadata.permalinkTemplate = sourceMetadata.permalinkTemplate;

        return targetMetadata;
    }

    private localizedContractToContract(defaultLocale: string, currentLocale: string, requestedLocale: string, localizedPageContract: LayoutLocalizedContract): LayoutContract {
        const locales = localizedPageContract[Constants.localePrefix];

        const metadata = (requestedLocale
            ? locales[requestedLocale]
            : locales[currentLocale])
            || this.copyMetadata(locales[defaultLocale], {});

        if (!metadata) {
            return null;
        }

        const contract: any = {
            key: localizedPageContract.key,
            ...metadata
        };

        return contract;
    }

    public async getLayoutByKey(key: string, requestedLocale?: string): Promise<LayoutContract> {
        if (!key) {
            throw new Error(`Parameter "key" not specified.`);
        }

        const layoutContract = await this.objectStorage.getObject<LayoutLocalizedContract>(key);

        if (!layoutContract) {
            return null;
        }

        const defaultLocale = await this.localeService.getDefaultLocaleCode();
        const currentLocale = await this.localeService.getCurrentLocaleCode();

        return this.localizedContractToContract(defaultLocale, currentLocale, requestedLocale, layoutContract);
    }

    private convertPage(localizedPage: Page<LayoutLocalizedContract>, defaultLocale: string, searchLocale: string, requestedLocale: string): Page<LayoutContract> {
        const resultPage: Page<LayoutContract> = {
            value: localizedPage.value.map(x => this.localizedContractToContract(defaultLocale, searchLocale, requestedLocale, x)),
            takeNext: async (): Promise<Page<LayoutContract>> => {
                const nextLocalizedPage = await localizedPage.takeNext();
                return this.convertPage(nextLocalizedPage, defaultLocale, searchLocale, requestedLocale);
            }
        };

        if (!localizedPage.takeNext) {
            resultPage.takeNext = null;
        }

        return resultPage;
    }

    public async search(query: Query<LayoutContract>, requestedLocale?: string): Promise<Page<LayoutContract>> {
        if (!query) {
            throw new Error(`Parameter "query" not specified.`);
        }

        const defaultLocale = await this.localeService.getDefaultLocaleCode();
        const currentLocale = await this.localeService.getCurrentLocaleCode();
        const searchLocale = requestedLocale || currentLocale;

        const localizedQuery = Utils.localizeQuery(query, searchLocale);

        try {
            const pageOfResults = await this.objectStorage.searchObjects<LayoutLocalizedContract>(this.layoutsPath, localizedQuery);
            return this.convertPage(pageOfResults, defaultLocale, searchLocale, requestedLocale);

        }
        catch (error) {
            throw new Error(`Unable to search layouts: ${error.stack || error.message}`);
        }
    }

    public async deleteLayout(layout: LayoutContract): Promise<void> {
        if (!layout) {
            throw new Error(`Parameter "layout" not specified.`);
        }

        const localizedLayoutContract = await this.objectStorage.getObject<LayoutLocalizedContract>(layout.key);

        if (localizedLayoutContract.locales) {
            const contentKeys = Object.values(localizedLayoutContract.locales).map(x => x.contentKey);

            for (const contentKey of contentKeys) {
                await this.objectStorage.deleteObject(contentKey);
            }
        }

        await this.objectStorage.deleteObject(layout.key);

        this.logger.trackEvent("LayoutDeleted", { key: layout.key });
    }

    public async createLayout(title: string, description: string, permalinkTemplate: string): Promise<LayoutContract> {
        const locale = await this.localeService.getDefaultLocaleCode();
        const identifier = Utils.guid();
        const layoutKey = `${this.layoutsPath}/${identifier}`;
        const contentKey = `${documentsPath}/${identifier}`;

        const localizedLayout: LayoutLocalizedContract = {
            key: layoutKey,
            locales: {
                [locale]: {
                    title: title,
                    description: description,
                    permalinkTemplate: permalinkTemplate,
                    contentKey: contentKey
                }
            }
        };

        await this.objectStorage.addObject<LayoutLocalizedContract>(layoutKey, localizedLayout);
        await this.objectStorage.addObject<Contract>(contentKey, layoutTemplate);

        this.logger.trackEvent("LayoutAdded", { key: layoutKey, contentKey: contentKey });

        const layoutContent: LayoutContract = {
            key: layoutKey,
            title: title,
            description: description,
            permalinkTemplate: permalinkTemplate,
            contentKey: contentKey
        };

        return layoutContent;
    }

    public async updateLayout(layout: LayoutContract, requestedLocale?: string): Promise<void> {
        if (!layout) {
            throw new Error(`Parameter "layout" not specified.`);
        }

        if (!requestedLocale) {
            requestedLocale = await this.localeService.getCurrentLocaleCode();
        }

        const layoutContract = await this.objectStorage.getObject<LayoutLocalizedContract>(layout.key);

        if (!layoutContract) {
            throw new Error(`Could not update layout. Layout with key "${layout.key}" doesn't exist.`);
        }

        const existingLocaleMetadata = layoutContract.locales[requestedLocale] || <LayoutMetadata>{};

        layoutContract.locales[requestedLocale] = this.copyMetadata(layout, existingLocaleMetadata);

        await this.objectStorage.updateObject<LayoutLocalizedContract>(layout.key, layoutContract);
    }

    public async getLayoutByPermalinkTemplate(permalinkTemplate: string): Promise<LayoutContract> {
        if (!permalinkTemplate) {
            throw new Error(`Parameter "permalinkTemplate" not specified.`);
        }

        const defaultLocale = await this.localeService.getDefaultLocaleCode();
        const query = Query
            .from<LayoutContract>()
            .where(`locales/${defaultLocale}/permalinkTemplate`, Operator.equals, permalinkTemplate);

        const pageOfObjects = await this.objectStorage.searchObjects<LayoutContract>(this.layoutsPath, query);
        const result = pageOfObjects.value;
        const layouts = Object.keys(result).map(key => result[key]);
        return layouts.length > 0 ? layouts[0] : null;
    }

    private sort(patterns: string[]): string[] {
        const result = [];

        function compare(a, b) {
            if (a.score < b.score) {
                return 1;
            }

            if (a.score > b.score) {
                return -1;
            }

            return 0;
        }

        for (const pattern of patterns) {
            const segments = pattern.split("/").filter(x => !!x);

            let score = 0;

            for (let i = 0; i < segments.length; i++) {
                const segment = segments[i];
                let weight;

                if (segment.startsWith("{")) { // variable
                    weight = 2;
                }
                else if (segment === "*") { // wildcard
                    weight = 1;
                }
                else { // constant
                    weight = 3;
                }

                score += weight / (i + 1);
            }

            result.push({ score: score, pattern: pattern });
        }

        return result.sort(compare).map(x => x.pattern);
    }

    private matchPermalink(permalink: string, template: string, locale?: string): PermalinkMatchResult {
        if (!template) {
            console.warn(`A layout with empty permalink template will be skipped.`);

            return {
                match: false,
                tokens: []
            };
        }

        if (locale) {
            const localePrefix = `/${locale}/`;

            if (permalink.startsWith(localePrefix)) {
                permalink = permalink.replace(localePrefix, "/");
            }
        }

        const tokens: PermalinkMatchToken[] = [];
        const permalinkSegments: string[] = permalink.split("/");
        const templateSegments: string[] = template.split("/");

        if (permalinkSegments.length !== templateSegments.length && template.indexOf("*") === -1) {
            return {
                match: false,
                tokens: tokens
            };
        }

        for (let i = 0; i < templateSegments.length; i++) {
            const permalinkSegment: string = permalinkSegments[i];
            const templateSegment: string = templateSegments[i];

            if (templateSegment === "*") { // wildcard
                if (permalinkSegment !== "" && permalinkSegment !== undefined) {
                    return {
                        match: true,
                        tokens: tokens
                    };
                }
                else {
                    return {
                        match: false,
                        tokens: []
                    };
                }
            }
            else if (templateSegment.startsWith("{")) { // variable
                tokens.push({ index: i, name: templateSegment.replace(/{|}/g, "") });
            }
            else if (permalinkSegment !== templateSegment) { // constant
                return {
                    match: false,
                    tokens: []
                };
            }
        }

        return {
            match: true,
            tokens: tokens
        };
    }

    public async getLayoutByPermalink(permalink: string, requestedLocale?: string): Promise<LayoutContract> {
        if (!permalink) {
            return null;
        }

        const defaultLocale = await this.localeService.getDefaultLocaleCode();
        const currentLocale = await this.localeService.getCurrentLocaleCode();

        const query = Query
            .from<LayoutContract>();

        const pageOfObjects = await this.objectStorage.searchObjects<LayoutLocalizedContract>(this.layoutsPath, query);
        const result = pageOfObjects.value;
        const layouts = Object.keys(result).map(key => result[key]);

        if (layouts && layouts.length) {
            let permalinkTemplates = layouts
                .map(x => x.locales[defaultLocale]?.permalinkTemplate) // We use permalinkTemplate from default locale only (for now).
                .filter(permalinkTemplate => !!permalinkTemplate);

            permalinkTemplates = this.sort(permalinkTemplates);

            const matchingTemplate = permalinkTemplates.find(template => {
                return this.matchPermalink(permalink, template, requestedLocale || currentLocale).match;
            });

            const matchingLayout = layouts.find(x => x.locales[defaultLocale].permalinkTemplate === (matchingTemplate || "/"));

            if (!matchingLayout) {
                return null;
            }

            return this.localizedContractToContract(defaultLocale, currentLocale, requestedLocale, matchingLayout);
        }
        else {
            return null;
        }
    }

    public async getLayoutContent(layoutKey: string, requestedLocale?: string): Promise<Contract> {
        if (!layoutKey) {
            throw new Error(`Parameter "layoutKey" not specified.`);
        }

        if (!requestedLocale) {
            requestedLocale = await this.localeService.getCurrentLocaleCode();
        }

        const defaultLocale = await this.localeService.getDefaultLocaleCode();
        const localizedLayoutContract = await this.objectStorage.getObject<LayoutLocalizedContract>(layoutKey);

        let layoutMetadata = localizedLayoutContract.locales[requestedLocale];

        if (!layoutMetadata) {
            layoutMetadata = localizedLayoutContract.locales[defaultLocale];
        }

        let layoutContent;

        if (layoutMetadata.contentKey) {
            layoutContent = await this.objectStorage.getObject<Contract>(layoutMetadata.contentKey);
        }
        else {
            const layoutDefaultLocaleMetadata = localizedLayoutContract.locales[defaultLocale];
            layoutContent = await this.objectStorage.getObject<Contract>(layoutDefaultLocaleMetadata.contentKey);
        }

        layoutContent.type = "layout";

        return layoutContent;
    }

    public async updateLayoutContent(layoutKey: string, content: Contract, requestedLocale?: string, changeDescription?: string): Promise<void> {
        if (!layoutKey) {
            throw new Error(`Parameter "layoutKey" not specified.`);
        }

        if (!content) {
            throw new Error(`Parameter "content" not specified.`);
        }

        const localizedLayoutContract = await this.objectStorage.getObject<LayoutLocalizedContract>(layoutKey);

        if (!localizedLayoutContract) {
            throw new Error(`Layout with key "${layoutKey}" not found.`);
        }

        if (!requestedLocale) {
            requestedLocale = await this.localeService.getCurrentLocaleCode();
        }

        let layoutMetadata = localizedLayoutContract.locales[requestedLocale];

        if (!layoutMetadata) {
            const defaultLocale = await this.localeService.getDefaultLocaleCode();
            const defaultLayoutMetadata = localizedLayoutContract.locales[defaultLocale];
            const identifier = Utils.guid();

            layoutMetadata = this.copyMetadata(defaultLayoutMetadata, {
                contentKey: `${documentsPath}/${identifier}`
            });

            localizedLayoutContract.locales[requestedLocale] = layoutMetadata;

            await this.objectStorage.updateObject(layoutKey, localizedLayoutContract);
        }
        else if (!layoutMetadata.contentKey) {
            const identifier = Utils.guid();
            layoutMetadata.contentKey = `${documentsPath}/${identifier}`;
            await this.objectStorage.updateObject(layoutKey, layoutMetadata);
        }

        await this.objectStorage.updateObject(layoutMetadata.contentKey, content, changeDescription);
    }

    public async copyLayout(key: string): Promise<LayoutContract> {
        const originalLayout = await this.objectStorage.getObject<LayoutLocalizedContract>(key);
        const identifier = Utils.guid();
        const targetKey = `${this.layoutsPath}/${identifier}`;

        const targetLayout: LayoutLocalizedContract = {
            key: targetKey,
            locales: {}
        };

        const sourceLocales = Object.keys(originalLayout.locales);

        for (const locale of sourceLocales) {
            const sourceMetadata = originalLayout.locales[locale];
            const sourceContentKey = sourceMetadata.contentKey;
            const sourceLayoutContent = await this.objectStorage.getObject<Contract>(sourceContentKey);

            const targetIdentifier = Utils.guid();
            const targetContentKey = `${documentsPath}/${targetIdentifier}`;

            const targetMetadata: LayoutMetadata = {
                title: sourceMetadata.title + " (copy)",
                description: sourceMetadata.description,
                contentKey: targetContentKey,
                permalinkTemplate: null
            };

            targetLayout.locales[locale] = targetMetadata;
            const targetLayoutContent = Objects.clone<Contract>(sourceLayoutContent);
            targetLayoutContent["key"] = targetContentKey;

            await this.objectStorage.addObject<Contract>(targetContentKey, targetLayoutContent);

            this.logger.trackEvent("LayoutCopied", { key: targetKey, sourceLayoutKey: key });
        }

        await this.objectStorage.addObject<LayoutLocalizedContract>(targetKey, targetLayout);

        return this.getLayoutByKey(targetKey);
    }
}
