import React from 'react';
import PropTypes from 'prop-types';

import IDUtil from './util/IDUtil'; // for generating unique CSS classnames for this component
import RegexUtil from './util/RegexUtil'; // for matching the initial search term with available transcripts

import LocalStorageHandler from './util/LocalStorageHandler'; // for updating the stored-visited-results
import SessionStorageHandler from './util/SessionStorageHandler';
import Events from './util/Events'; // for updating the stored-visited-results

import DocumentAPI from './api/DocumentAPI'; // for fetching the resource from the search API
import ProjectAPI from './api/ProjectAPI'; // for fetching the active project (user can change the project in the resource viewer)
import PlayoutAPI from './api/PlayoutAPI'; // for requesting access to the play-out proxy

import CollectionUtil from './util/CollectionUtil'; // for generating a CollectionConfig object for the current resource (after loading the resource)

import ViewerBase from './components/resourceviewer/ViewerBase';
import Loading from './components/shared/Loading';
import Query from './model/Query';

import { ResourceViewerContext } from './components/resourceviewer/ResourceViewerContext';

import {AnnotationClient, AnnotationEvents, MSAnnotationUtil} from './components/resourceviewer/AnnotationClient';

import { ANNOTATION_TYPE, SESSION_STORAGE_ACTIVE_TYPES } from './util/AnnotationConstants';
import { ENTITY_TYPE } from './util/EntityConstants';

import TranscriptUtil from "./util/TranscriptUtil";
/*
	This component gets the following from the URL:
	- resource ID
	- collection ID
	- search term
	- fragement url (@deprecated, ignore for now)

	Mainly this component fetches all the necessary data for the ViewerBase (drawing all the awesome new stuff):
	- user (present in this compoment's props)
	- (optional) query (fetch from local storage or construct one from the URL param: search term)
	- user projects (list of projects owned by the current user)
	- resource (containing metadata + playlist of media objects + more stuff, see SearchResult.js in the model package)
	- collectionConfig (TODO merge with resource object)

	Simple stuff like loading a 404 and loading graphic is rendered here (but might as well be done in the ViewerBase as well)

*/
export const ResourceEvents = {
    // When entities were asynchronously loaded, notify listeners to update their resource representation
    SET_ENTITIES: 'set_entities'
};


export default class ResourceViewer extends React.Component {

    constructor(props) {
        super(props);
        const storedSearchResults = LocalStorageHandler.getJSONFromLocalStorage('stored-search-results');
        const singleResource = window.location.search.indexOf('singleResource') > -1;
        const query = !singleResource && storedSearchResults ? storedSearchResults.query : null;
        this.state = {
            isLoading: true, // whenever data is being fetched and a loading graphic needs to be shown
            found: false, // whenever the DocumentAPI returns a 404 reflect this in this boolean
            activeProject: LocalStorageHandler.getJSONFromLocalStorage(
                'stored-active-project'
            ), // the currently selected project || null
            activeMediaObject: null, // nothing selected by default, is populated after the ViewerColumn finishes initialisation
            query, // the query.term can be used for highlighting parts of the metadata
            singleResource, // load just a single resource, not a results set
            userProjects: [], // list of the user's projects (loaded via the ProjectAPI)
            resource: null, // instance of SearchResult (TODO change class name later)
            collectionConfig: null, // should be part of the resource/SearchResult (TODO update the constructor of SearchResult)
            playerAPI : null, //will be populated (via setPlayerAPI) by the MediaColumn when the player is ready!
            activeAnnotationTypes: this.getActiveAnnotationTypes(),
        };

        //store the search term in the session so the annotation columns can get it
        const SESSION_SEARCH_TERM = 'bg__content-annotations-search-term';
        if(query) { // empty on corrupted queries?
            SessionStorageHandler.set(SESSION_SEARCH_TERM, query.term)
        }

        this.mediaEvents = new Events();
        this.resourceEvents = new Events();
        this.CLASS_PREFIX = 'RV'; // please use this when generating unique class names for this component
    }

    async componentDidMount() {
        // first create a Query object from the search term URL parameter
        const query = this.state.query
            ? this.state.query
            : this.props.params.st
            ? Query.construct({ term: this.props.params.st }) // query from local storage or construct based on URL param
            : null; // this item was accessed not via the search page, so leave the query out

        // then fetch the user projects to populate the project selection at the top of the page
        const userProjects = await this.loadUserProjects(this.props.user);

        // then construct a collection config (so the resource can be formatted for the UI on retrieval)
        const collectionConfig = await this.generateCollectionConfig(
            this.props.clientId,
            this.props.user,
            this.props.params.cid
        );

        // then load the resource (actually a SearchResult object...)
        const resource = await this.loadResource(
            this.props.params.id,
            this.props.params.cid,
            collectionConfig,
            query,
            this.props.params.l
        );

        // set the state to something 404'ish if the resource could not be retrieved
        if (!resource || !resource.resourceId) {
            this.setState({
                isLoading: false, // loading graphic should be disabled
                found: false, // so the render method knows that the resource could not be retrieved (e.g. because of invalid ids OR server problems)
                query: query, // so the 404 message can also include a reference to the original query
                userProjects: userProjects, // it's ok to render the list of projects, but could be hidden as well
                resource: null, // reset
                collectionConfig: null // reset
            });
            return;
        }

        const activeMediaObject = this.determineInitialMediaObject(
            resource,
            this.props.params,
            collectionConfig
        );

        //add the entities to the resource (load them via grlc queries)
        //const resourceEntities = await this.fetchEntitiesInResource(collectionConfig, collectionConfig.getResourceUri(resource));
        //resource.setEntities(resourceEntities);

        //this time provice a callback function
        this.fetchEntitiesInResource(
            collectionConfig,
            collectionConfig.getResourceUri(resource),
            entities => {
                console.debug('All entities were fetched, now setting the entities');
                this.setResourceEntities(entities)
            }
        );


        // the annotation client is a singleton, so always the same instance is returned (with updated props)
        this.annotationClient = AnnotationClient.singleton(
            this.props.recipe.ingredients.annotationConfig,
            this.mediaEvents //augment the media events with annotation event handling
        );

        // set the state with everything that got successfully loaded
        this.setState(
            {
                isLoading: false, // hide the loading graphic
                found: true, // proudly show the resource was found, by drawing the new awesome components on screen!
                query: query, // always nice to be able to reference the query leading up to this resource
                userProjects: userProjects, // for populating the ProjectList.jsx component (or something more fancy)
                resource: resource, // contains all the metadata + playlist of media objects + more
                collectionConfig: collectionConfig // use this properly format resource data for the particular collection
            },
            () => {
                // finally add the resource to the list of visited items
                LocalStorageHandler.pushItemToLocalStorage(
                    'stored-visited-results',
                    resource.resourceId
                );

                // set the first MediaObject to active
                if (
                    resource.playableContent &&
                    resource.playableContent.length > 0
                ) {
                    this.setActiveMediaObject(activeMediaObject, true); //resource.playableContent[0]
                }
            }
        );
    }

    /* ----------------------------- DETERMINE INITIAL MEDIA OBJECT ------------------------ */

    //turns url params into a media fragment for the resource viewer to highlight
    //otherwise: look for the first matching media object/fragment in the entirety of the resource
    determineInitialMediaObject = (resource, urlParams, collectionConfig) => {
        const mediaObjects = resource.playableContent || [];
        if (mediaObjects.length === 0) return null;

        let activeMediaObject = null;

        //first try to simpy fetch the mediaObject indicated by the URL params
        if (urlParams.contentId !== undefined) {
            activeMediaObject = mediaObjects.find(
                mo => mo.contentId === urlParams.contentId
            );
            //if the client side mapping did not add a mediafragment, create one here
            if(activeMediaObject) {
                activeMediaObject.mediaFragments = this.__toMediaFragments(urlParams);
            }
        } else if(typeof urlParams.st === 'string' && urlParams.st.length >= 2) {
            //then see if a media object can be fetched by checking the whole resource
            activeMediaObject = collectionConfig.findMatchingMediaFragments(resource, urlParams.st);
        }
        return activeMediaObject ? activeMediaObject : mediaObjects[0]; //just return the first if nothing was found
    };

    __toMediaFragments = urlParams => {
        if (urlParams.s !== undefined || urlParams.e !== undefined) {
            return [{
                start : urlParams.s !== undefined ? urlParams.s : 0,
                end : urlParams.e !== undefined ? urlParams.e : 0
            }]
        } else if (urlParams.x !== undefined && urlParams.y !== undefined && urlParams.w !== undefined && urlParams.h !== undefined) {
            return [{
                x : urlParams.x,
                y : urlParams.y,
                w : urlParams.w,
                h : urlParams.h
            }]
        }
    };

    /* -------------------------------- FIND THE ENTITIES IN THE RESOURCE -------------------------- */

    fetchEntitiesInResource = async(collectionConfig, resourceURI, callback=null) => {
        const entities = {}
        for(const et in ENTITY_TYPE) {//NOTE: async does not easily work in a forEach
            let entitiesOfType = await this.__fetchEntitiesOfType(collectionConfig, resourceURI, ENTITY_TYPE[et]);
            if(entitiesOfType != null) { //only assign if not null
                entities[ENTITY_TYPE[et]] = entitiesOfType
            }
        }
        if(callback && typeof(callback) === "function") {
            console.debug('calling back the caller with some delicious entities', entities);
            callback(entities)
        }
        return entities;
    };

    __fetchEntitiesOfType = (collectionConfig, resourceURI, entityType) => {
        return new Promise((resolve) => {
            const entityTypeConfig = this.__getEntityTypeConfig(collectionConfig, entityType);
            if(entityTypeConfig && typeof(entityTypeConfig.fetchEntitiesInResource) === "function") {
                entityTypeConfig.fetchEntitiesInResource(
                    resourceURI,
                    resolve
                );
            } else {
                resolve(null);
            }
        });
    };

    __getEntityTypeConfig = (collectionConfig, entityType) => {
        const entityConfig = collectionConfig.getEntityConfig();
        if(!entityConfig || !entityConfig[entityType]) return null;

        return entityConfig[entityType];
    };

    setResourceEntities = entities => {
        this.resourceEvents.trigger(
            ResourceEvents.SET_ENTITIES,
            entities
        );
        const resource = this.state.resource;
        resource.setEntities(entities)
        this.setState({resource : resource});
    };

    /* ----------------------------- CONTEXT MANIPULATION FUNCTIONS ------------------------ */

    setActiveProject = async (project) => {
        //first make sure to reload the annotations from the server
        await this.annotationClient.setActiveTarget(
            [
                this.state.collectionConfig ? this.state.collectionConfig.getSearchIndex() : null,
                this.state.resource ? this.state.resource.resourceId : null,
                this.state.activeMediaObject ? this.state.activeMediaObject.assetId : null
            ],
            {
                user : this.props.user.id,
                project: project ? project.id : null
            },
            this.state.activeMediaObject ?
                this.__extractAnnotationTypes(this.state.activeMediaObject.mimeType) :
                null
        );

        this.setState({
            activeProject: project
        });

        // store active project to localstorage
        LocalStorageHandler.storeJSONInLocalStorage(
            'stored-active-project',
            project
        );
    };

    setActiveMediaObject = async (mediaObject, initPage=false) => {
        if (!mediaObject) {
            throw 'Could not set empty mediaobject as active';
        }

        //await the playout access
        if(this.state.collectionConfig) {
            if(this.state.collectionConfig.requiresPlayoutAccess()) {
                if(mediaObject.requiresPlayoutAccess === false) {
                    mediaObject.playoutAccess = true;
                } else {
                    mediaObject.playoutAccess = await this.requestPlayoutAccess(mediaObject);
                }
            } else {
                mediaObject.playoutAccess = true;
            }
        } else {
            mediaObject.playoutAccess = true;
        }

        //now check if the active object matches the url content ID, so the fragment can be highlighted (again)
        //FIXME: this does not work properly yet!
        if(this.props.params.contentId === mediaObject.contentId) {
            mediaObject.mediaFragments = this.__toMediaFragments(this.props.params);
        } else if(typeof this.props.params.st === 'string' && this.props.params.st.length >= 2) {
            //TODO add an extra check to see if the user paged manually

            //then see if a media object can be fetched by checking the whole resource
            const matchingFragment = this.state.collectionConfig.findMatchingMediaFragments(
                this.state.resource,
                this.props.params.st,
                mediaObject //make sure the match is within the active media object
            );
            //only assign the fragment if it matches the retrieved first hit
            if(matchingFragment && matchingFragment.contentId === mediaObject.contentId) {
                mediaObject.mediaFragment = matchingFragment ? matchingFragment.mediaFragment : null;
            }
        }

        //update the target in the annotation client (also making the client refetch annotations related to that target)
        //TODO basically here the known local context should be translated into a nested PID.
        //NOTE: clients of STAN should think about the resource structure
        await this.annotationClient.setActiveTarget(
            [
                this.state.collectionConfig ? this.state.collectionConfig.getSearchIndex() : null,
                this.state.resource ? this.state.resource.resourceId : null,
                mediaObject.assetId
            ],
            {
                user : this.props.user.id,
                project: this.state.activeProject ? this.state.activeProject.id : null
            },
            this.__extractAnnotationTypes(mediaObject.mimeType)
        )

        //now set the state and then resolve to the callee
        return new Promise(resolve => {
            this.setState({
                activeMediaObject: mediaObject,
                //transcript : transcript,
                //transcriptFirstHit : transcriptFirstHit //TODO should be removed and derived from activeMediaObject.fragment!!!
            }, resolve(mediaObject));
        })
    };

    __extractAnnotationTypes = mimeType => {
        if(!mimeType) return [];
        if(mimeType.indexOf('video') !== -1) {
            return ['Video']
        } else if(mimeType.indexOf('audio') !== -1) {
            return ['Audio']
        } else if(mimeType.indexOf('image') !== -1) {
            return ['Image']
        }
        return [];
    };

    //so all components can control the player for fancy features!
    setPlayerAPI = playerAPI => this.setState({playerAPI});

    /* ----------------------------- FUNCTIONS FOR SEQUENTIALLY LOADING ALL THE REQUIRED STATE INFORMATION ------------------------ */

    loadUserProjects = user => {
        return new Promise(resolve => {
            if (!user || !user.id) resolve(null);

            ProjectAPI.list(user.id, null, resolve);
        });
    };

    generateCollectionConfig = (clientId, user, collectionId) => {
        return new Promise(resolve => {
            if (!clientId || !user || !collectionId) resolve(null);

            CollectionUtil.generateCollectionConfig(
                clientId,
                user,
                collectionId,
                resolve
            );
        });
    };

    loadResource = (
        resourceId,
        collectionId,
        collectionConfig,
        query = null,
        layerName = null
    ) => {
        return new Promise(resolve => {
            if (!resourceId || !collectionId || !collectionConfig)
                resolve(null);

            DocumentAPI.getResource(
                layerName ? collectionId + '__' + layerName : collectionId, // collectionId (with optional layer suffix)
                resourceId,
                collectionConfig,
                query,
                resolve // pass the resolve function as the callback for the API
            );
        });
    };

    //this one is tied to the setActiveMediaObject()
    requestPlayoutAccess = mediaObject => {
        return new Promise(resolve => {
            if(mediaObject.mimeType.startsWith('application')) {
                resolve(true, null);
            } else if (mediaObject.contentServerId === undefined || mediaObject.contentId === undefined) {
                resolve(true, null); //e.g. youtube or vimeo playable content does not have a contentServerId or contentId
            } else { // if there is a contentServerId + contentId it means the playout proxy provide access
                PlayoutAPI.requestAccess(
                    mediaObject.contentServerId,
                    mediaObject.contentId,
                    null,
                    resolve
                );
            }
        });
    };

    // ---------------------------------- TRANSCRIPT STATUS FUNCTIONS -------------------------------
    getActiveTranscripts = () => {
        //TODO currently we're checking if we can allow transcripts even though there is no active media object
        if(this.state.resource.transcripts && this.state.activeMediaObject) {
            return this.state.resource.transcripts[this.state.activeMediaObject.assetId];
        }
        return null;
    };

    getActiveTranscript = (transcriptType=null, matchTitle=false) => {
        const activeTranscripts = this.getActiveTranscripts(); //FIXME ASR for DAAN/IMMIX needs to be fixed on the server
        if(!activeTranscripts || Object.keys(activeTranscripts).length === 0) return null;

        const typeIndex = activeTranscripts.findIndex(
            t => matchTitle ? t.title === transcriptType : t.type === transcriptType
        )

        //if no type is specified, just return the first one (TODO test with 2e kamer, which has multiple transcripts)
        return transcriptType && typeIndex !== -1 ?
            activeTranscripts[typeIndex] :
            activeTranscripts[0];
    };

    getFirstHitInTranscript = (searchTerm, transcript) => {
        if(!searchTerm || searchTerm.length < 2 || !transcript || !transcript.lines) {
            return null;
        }
        let firstHit = null;
        let regex = null;
        try {
            regex = RegexUtil.generateRegexForSearchTerm(searchTerm.toLowerCase());
        } catch (err) {
            firstHit = transcript.lines[0];
        }
        firstHit = regex ? transcript.lines.find(item => item.text.toLowerCase().search(regex) !== -1) : null;
        return firstHit ? Math.floor(firstHit.start / 1000): -1; //return in seconds
    };

    /* ----------------------------- ANNOTATION TYPES ------------------------ */

    // get active types from local storage
    getActiveAnnotationTypes(){
        return SessionStorageHandler.getSplit(SESSION_STORAGE_ACTIVE_TYPES,  Object.values(ANNOTATION_TYPE));
    }

    // set active annotations to state and store to session storage
    setActiveAnnotationTypes = (activeAnnotationTypes) =>{
        this.setState({activeAnnotationTypes});
        SessionStorageHandler.set(SESSION_STORAGE_ACTIVE_TYPES, activeAnnotationTypes.join(","));
    }

    /* ----------------------------- RENDER FUNCTIONS ------------------------ */

    renderResourceNotFound = () => (
        <div className={IDUtil.cssClassName('not-found', this.CLASS_PREFIX)}>
            The resource could not be found
        </div>
    );

    renderLoadingGraphic = () => <Loading />;

    renderViewerbase = (resource, collectionConfig) => {
        if (!resource || !collectionConfig) return null;

        return <ViewerBase />;
    };

    render() {
        const loadingGraphic = this.state.isLoading
            ? this.renderLoadingGraphic()
            : null;

        const notFoundMsg =
            this.state.found === false && this.state.isLoading === false
                ? this.renderResourceNotFound()
                : null;
        const viewerBase = this.renderViewerbase(
            this.state.resource,
            this.state.collectionConfig
        );

        return (
            <div className={IDUtil.cssClassName('resource-viewer')}>
                {loadingGraphic}
                {notFoundMsg}
                <ResourceViewerContext.Provider
                    value={{
                        //specific RV props
                        singleResource: this.state.singleResource,
                        resourceEvents: this.resourceEvents,
                        mediaEvents: this.mediaEvents,

                        //kind of stable properties also used elsewhere in the MS
                        urlParams : this.props.params,
                        recipe: this.props.recipe,
                        user: this.props.user,
                        userProjects: this.state.userProjects,
                        query: this.state.query,
                        collectionConfig: this.state.collectionConfig,
                        resource: this.state.resource,

                        // active annotation types
                        activeAnnotationTypes: this.state.activeAnnotationTypes,
                        setActiveAnnotationTypes: this.setActiveAnnotationTypes,

                        // setting a project affects most components
                        activeProject: this.state.activeProject,
                        setActiveProject: this.setActiveProject,

                        // changing the media object affects most components
                        setActiveMediaObject: this.setActiveMediaObject,
                        activeMediaObject: this.state.activeMediaObject,

                        // annotation & segment related props are stored in the annotation client
                        annotationClient: this.annotationClient,

                        getActiveTranscripts : this.getActiveTranscripts,
                        getActiveTranscript : this.getActiveTranscript,
                        getFirstHitInTranscript : this.getFirstHitInTranscript,

                        //playerAPI can be used everywhere!
                        playerAPI : this.state.playerAPI,
                        setPlayerAPI : this.setPlayerAPI

                    }}
                >
                    {viewerBase}
                </ResourceViewerContext.Provider>
            </div>
        );
    }
}

ResourceViewer.propTypes = {
    clientId: PropTypes.string.isRequired, // Required for generating the collection config

    user: PropTypes.shape({
        // required for several API calls
        id: PropTypes.string.isRequired
    }).isRequired,

    params: PropTypes.shape({
        // these are params passed via the URL the params
        cid: PropTypes.string.isRequired,
        id: PropTypes.string.isRequired,
        st: PropTypes.string,
        fragmentUrl: PropTypes.string //replace this with media object ID (a.k.a. asset ID)
    }).isRequired,

    recipe: PropTypes.shape({
        // for now the recipe ingredients are needed for the FlexPlayer; TODO remove later
        description: PropTypes.string,
        id: PropTypes.string,
        inRecipeList: PropTypes.bool,
        name: PropTypes.string.isRequired,
        phase: PropTypes.string,
        recipeDescription: PropTypes.string,
        type: PropTypes.string,
        url: PropTypes.string,
        ingredients: PropTypes.shape({
            useProjects: PropTypes.bool, // should use, but forget it for now
            annotationConfig: PropTypes.object // updated for STAN
        }).isRequired
    }).isRequired
};
