import { Injectable } from '@angular/core';

import { TranslateService } from '@ngx-translate/core';

import { BaseModel } from 'app/shared/models/base/base-model';
import { ProjectionDefault } from 'app/shared/models/core/projection-default';
import {
    elementIdentifies,
    IdentifiableProjectorElement,
    Projector,
    ProjectorElement,
    ProjectorElements
} from 'app/shared/models/core/projector';
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
import {
    isProjectable,
    isProjectorElementBuildDeskriptor,
    Projectable,
    ProjectorElementBuildDeskriptor
} from 'app/site/base/projectable';
import { SlideManager } from 'app/slides/services/slide-manager.service';
import { ConfigService } from '../ui-services/config.service';
import { DataStoreService } from './data-store.service';
import { HttpService } from './http.service';
import { ProjectorDataService } from './projector-data.service';
import { ViewModelStoreService } from './view-model-store.service';

/**
 * This service cares about Projectables being projected and manage all projection-related
 * actions.
 *
 * We cannot access the ProjectorRepository here, so we will deal with plain projector objects.
 */
@Injectable({
    providedIn: 'root'
})
export class ProjectorService {
    /**
     * Constructor.
     *
     * @param DS
     * @param dataSend
     */
    public constructor(
        private DS: DataStoreService,
        private http: HttpService,
        private slideManager: SlideManager,
        private viewModelStore: ViewModelStoreService,
        private translate: TranslateService,
        private configService: ConfigService,
        private projectorDataService: ProjectorDataService
    ) {}

    /**
     * Retusn the identifiable projector element from the given types of slides/elements/descriptors
     *
     * @param obj Something related to IdentifiableProjectorElement
     * @returns the identifiable projector element from obj.
     */
    private getProjectorElement(
        obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
    ): IdentifiableProjectorElement {
        if (isProjectable(obj)) {
            return obj.getSlide(this.configService).getBasicProjectorElement({});
        } else if (isProjectorElementBuildDeskriptor(obj)) {
            return obj.getBasicProjectorElement({});
        } else {
            return obj;
        }
    }

    /**
     * Checks, if a given object is projected.
     *
     * @param obj The object in question
     * @returns true, if the object is projected on one projector.
     */
    public isProjected(obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement): boolean {
        const element = this.getProjectorElement(obj);
        return this.DS.getAll<Projector>('core/projector').some(projector => {
            return projector.isElementShown(element);
        });
    }

    /**
     * Get all projectors where the object is prejected on.
     *
     * @param obj The object in question
     * @return All projectors, where this Object is projected on
     */
    public getProjectorsWhichAreProjecting(
        obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
    ): Projector[] {
        const element = this.getProjectorElement(obj);
        return this.DS.getAll<Projector>('core/projector').filter(projector => {
            return projector.isElementShown(element);
        });
    }

    /**
     * Checks, if the object is projected on the given projector.
     *
     * @param obj The object
     * @param projector The projector to test
     * @returns true, if the object is projected on the projector.
     */
    public isProjectedOn(
        obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement,
        projector: Projector
    ): boolean {
        return projector.isElementShown(this.getProjectorElement(obj));
    }

    /**
     * Projects the given ProjectorElement on the given projectors. Removes the element
     * from all non-given projectors
     *
     * @param projectors All projectors where to add the element.
     * @param element The element in question.
     */
    public projectOnMultiple(projectors: Projector[], element: IdentifiableProjectorElement): void {
        this.DS.getAll<Projector>('core/projector').forEach(projector => {
            if (projectors.includes(projector)) {
                this.projectOn(projector, element);
            } else if (projector.isElementShown(element)) {
                this.removeFrom(projector, element);
            }
        });
    }

    /**
     * Projcets the given object on the projector. If the object is non-stable, all other non-stable
     * elements will be removed and added to the history.
     *
     * @param projector The projector to add the object to.
     * @param obj The object to project
     */
    public async projectOn(
        projector: Projector,
        obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
    ): Promise<void> {
        const element = this.getProjectorElement(obj);

        if (element.stable) {
            // remove the same element, if it is currently projected
            projector.removeElements(element);
            // Add this stable element
            projector.addElement(element);
            await this.projectRequest(projector, projector.elements);
        } else {
            // For non-stable elements remove all other non-stable elements, add them to the history and
            // add the one new element to the projector.
            const removedElements = projector.removeAllNonStableElements();
            let changed = removedElements.length > 0;

            if (element) {
                projector.addElement(element);
                changed = true;
            }
            if (changed) {
                await this.projectRequest(projector, projector.elements, null, removedElements, false, true);
            }
        }
    }

    /**
     * Removes the given object from the projector. Non stable elements will be added to the history.
     *
     * @param projector The projector
     * @param obj the object to unproject
     */
    public async removeFrom(
        projector: Projector,
        obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
    ): Promise<void> {
        const element = this.getProjectorElement(obj);

        const removedElements = projector.removeElements(element);
        if (removedElements.length > 0) {
            if (element.stable) {
                await this.projectRequest(projector, projector.elements);
            } else {
                // For non-stable elements: Add removed elements to the history.
                await this.projectRequest(projector, projector.elements, null, removedElements);
            }
        }
    }

    public async updateElement(
        projector: Projector,
        obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
    ): Promise<void> {
        const element = this.getProjectorElement(obj);
        projector.replaceElements(element);
        await this.projectRequest(projector, projector.elements, projector.elements_preview);
    }

    /**
     * Executes the request to change projector elements.
     *
     * Note: Just one of `appendToHistory` and `deleteLastHistoryElement` can be given.
     *
     * @param projector The affected projector
     * @param elements (optional) Elements to set.
     * @param preview (optional) preview to set
     * @param appendToHistory (optional) Elements to be appended to the history
     * @param deleteLastHistroyElement (optional) If given, the last history element will be removed.
     */
    private async projectRequest(
        projector: Projector,
        elements?: ProjectorElements,
        preview?: ProjectorElements,
        appendToHistory?: ProjectorElements,
        deleteLastHistroyElement?: boolean,
        resetScroll?: boolean
    ): Promise<void> {
        const requestData: any = {};
        if (elements) {
            requestData.elements = this.cleanupElements(projector, elements);
        }
        if (preview) {
            requestData.preview = preview;
        }
        if (appendToHistory && appendToHistory.length) {
            requestData.append_to_history = appendToHistory;
        }
        if (deleteLastHistroyElement) {
            requestData.delete_last_history_element = true;
        }
        if (appendToHistory && appendToHistory.length && deleteLastHistroyElement) {
            throw new Error('You cannot append to the history and delete the last element at the same time');
        }
        if (resetScroll) {
            requestData.reset_scroll = resetScroll;
        }
        await this.http.post(`/rest/core/projector/${projector.id}/project/`, requestData);
    }

    /**
     * Cleans up stable elements with errors from the projector
     *
     * @param projector The projector
     * @param elements The elements to clean up
     * @reutns the cleaned up elements.
     */
    private cleanupElements(projector: Projector, elements: ProjectorElements): ProjectorElements {
        const projectorData = this.projectorDataService.getAvailableProjectorData(projector);

        if (projectorData) {
            projectorData.forEach(entry => {
                if (entry.data.error && entry.element.stable) {
                    // Remove this element
                    const idElementToRemove = this.slideManager.getIdentifialbeProjectorElement(entry.element);
                    elements = elements.filter(element => {
                        return !elementIdentifies(idElementToRemove, element);
                    });
                }
            });
        }

        return elements;
    }

    /**
     * Given a projectiondefault, we want to retrieve the projector, that is assigned
     * to this default.
     *
     * @param projectiondefault The projection default
     * @return the projector associated to the given projectiondefault.
     */
    public getProjectorForDefault(projectiondefault: string): Projector | null {
        const pd = this.DS.find(ProjectionDefault, _pd => _pd.name === projectiondefault);
        if (pd) {
            return this.DS.get<Projector>(Projector, pd.projector_id);
        } else {
            return null;
        }
    }

    /**
     * Asserts, that the given element is mappable to a model or view model.
     * Throws an error, if this assertion fails.
     *
     * @param element The element to check
     */
    private assertElementIsMappable(element: IdentifiableProjectorElement): void {
        if (!this.slideManager.canSlideBeMappedToModel(element.name)) {
            throw new Error('This projector element cannot be mapped to a model');
        }
        const identifiers = element.getIdentifiers();
        if (!identifiers.includes('name') || !identifiers.includes('id')) {
            throw new Error('To map this element to a model, a name and id is needed.');
        }
    }

    /**
     * Returns a model associated with the identifiable projector element. Throws an error,
     * if the element is not mappable.
     *
     * @param element The projector element
     * @returns the model from the projector element
     */
    public getModelFromProjectorElement<T extends BaseModel>(element: IdentifiableProjectorElement): T {
        this.assertElementIsMappable(element);
        return this.DS.get<T>(element.name, element.id);
    }

    /**
     * Returns a view model associated with the identifiable projector element. Throws an error,
     * if the element is not mappable.
     *
     * @param element The projector element
     * @returns the view model from the projector element
     */
    public getViewModelFromProjectorElement<T extends BaseProjectableViewModel>(
        element: IdentifiableProjectorElement
    ): T {
        this.assertElementIsMappable(element);
        const viewModel = this.viewModelStore.get<T>(element.name, element.id);
        if (viewModel && !isProjectable(viewModel)) {
            console.error('The view model is not projectable', viewModel, element);
        }
        return viewModel;
    }

    /**
     */
    public getSlideTitle(element: ProjectorElement): string {
        if (this.slideManager.canSlideBeMappedToModel(element.name)) {
            const idElement = this.slideManager.getIdentifialbeProjectorElement(element);
            const viewModel = this.getViewModelFromProjectorElement(idElement);
            if (viewModel) {
                return viewModel.getProjectorTitle();
            }
        }
        const configuration = this.slideManager.getSlideConfiguration(element.name);
        if (configuration.getSlideTitle) {
            return configuration.getSlideTitle(element, this.translate, this.viewModelStore);
        }

        return this.translate.instant(this.slideManager.getSlideVerboseName(element.name));
    }

    /**
     * Projects the next slide in the queue. Moves all currently projected
     * non-stable slides to the history.
     *
     * @param projector The projector
     */
    public async projectNextSlide(projector: Projector): Promise<void> {
        await this.projectPreviewSlide(projector, 0);
    }

    /**
     * Projects one slide (given by the index of the preview) on the given projector. Moves
     * all current projected non-stable elements to the history.
     *
     * @param projector The projector
     * @param previewIndex The index in the `elements_preview` array.
     */
    public async projectPreviewSlide(projector: Projector, previewIndex: number): Promise<void> {
        if (projector.elements_preview.length === 0 || previewIndex >= projector.elements_preview.length) {
            return;
        }

        const removedElements = projector.removeAllNonStableElements();
        projector.addElement(projector.elements_preview.splice(previewIndex, 1)[0]);
        await this.projectRequest(
            projector,
            projector.elements,
            projector.elements_preview,
            removedElements,
            false,
            true
        );
    }

    /**
     * Projects the last slide of the history. This slide will be removed from the history.
     *
     * @param projector The projector
     */
    public async projectPreviousSlide(projector: Projector): Promise<void> {
        if (projector.elements_history.length === 0) {
            return;
        }
        // Get the last element from the history
        const lastElements: ProjectorElements = projector.elements_history[projector.elements_history.length - 1];
        let lastElement: ProjectorElement = null;
        if (lastElements.length > 0) {
            lastElement = lastElements[0];
        }

        // Add all current elements to the preview.
        const removedElements = projector.removeAllNonStableElements();
        removedElements.forEach(e => projector.elements_preview.unshift(e));

        // Add last element
        if (lastElement) {
            projector.addElement(lastElement);
        }
        await this.projectRequest(projector, projector.elements, projector.elements_preview, null, true, true);
    }

    /**
     * Saves the preview of the projector
     *
     * @param projector The projector to save the preview.
     */
    public async savePreview(projector: Projector): Promise<void> {
        await this.projectRequest(projector, null, projector.elements_preview);
    }

    /**
     * Appends the given element to the preview.
     *
     * @param projector The projector
     * @param element The element to add to the preview.
     */
    public async addElementToPreview(projector: Projector, element: ProjectorElement): Promise<void> {
        projector.elements_preview.push(element);
        await this.projectRequest(projector, null, projector.elements_preview);
    }
}
