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 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 { initHelp } from './components/workspace/helpers/helpDoc';
/*
	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 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)
            contextChange: null
        };

        this.mediaEvents = 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
        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
                contextChange: null
            });
            return;
        }

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

        // 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
                contextChange: null,
                activeMediaObject: activeMediaObject
            },
            () => {
                // 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(resource.playableContent[0]);
                }
            }
        );
        initHelp("Resource viewer", "/feature-doc/howtos/resource-viewer");
    }

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

    // checks if mediaObject with assetId in URL params can be found
    // if not: tries to match the first mediaObject with a transcript matching the search term
    // the media object is returned with the spatial or temporal properties passed via the URL params (if any)
    determineInitialMediaObject = (resource, urlParams) => {
        const mediaObjects = resource.playableContent || [];
        if (mediaObjects.length === 0) return null;

        let activeMediaObject = null;
        const transcripts = resource.rawData.layer__asr || null;
        if (urlParams.assetId !== undefined) {
            activeMediaObject = mediaObjects.find(
                mo => mo.assetId === urlParams.assetId
            );

            if (urlParams.s !== undefined || urlParams.e !== undefined) {
                activeMediaObject.start = urlParams.s ? urlParams.s : 0;
                activeMediaObject.end = urlParams.e ? urlParams.e : 0;
            } else if (
                urlParams.x !== undefined &&
                urlParams.y !== undefined &&
                urlParams.w !== undefined &&
                urlParams.h !== undefined
            ) {
                activeMediaObject.x = urlParams.x;
                activeMediaObject.y = urlParams.y;
                activeMediaObject.w = urlParams.w;
                activeMediaObject.h = urlParams.h;
            }
        } else if (
            typeof urlParams.st === 'string' &&
            urlParams.st.length >= 2 &&
            transcripts
        ) {
            activeMediaObject = this.fetchMediaObjectMatchingTranscript(
                mediaObjects,
                transcripts,
                urlParams.st
            );
        }
        return activeMediaObject ? activeMediaObject : mediaObjects[0]; //just return the first if nothing was found
    };

    // returns the first media object that matches the search term in the accompanied transcript
    fetchMediaObjectMatchingTranscript = (
        mediaObjects,
        transcripts,
        initialSearchTerm
    ) => {
        let regex = null;
        try {
            regex = RegexUtil.generateRegexForSearchTerm(initialSearchTerm);
        } catch (err) {
            console.debug('invalid regex');
        }
        const firstMatch = regex
            ? transcripts.find(phrase => {
                  return phrase.words.search(regex) !== -1;
              })
            : null;
        return firstMatch
            ? mediaObjects.find(mo => mo.assetId === firstMatch.carrierId)
            : null;
    };

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

    async setActiveProject(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,
            contextChange: 'activeProject'
        });

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

    async setActiveMediaObject(mediaObject) {
        if (!mediaObject) {
            throw 'Could not set empty mediaobject as active';
        }

        if (mediaObject.playoutAccess === false) {
            mediaObject.playoutAccess = await this.requestPlayoutAccess(
                mediaObject
            ); // TODO set active media object to the first one
        }

        //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)
        )

        this.setState({
            activeMediaObject: mediaObject,
            contextChange: 'activeMediaObject'
        });
    }

    __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 [];
    }

    /* ----------------------------- 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 => {
            PlayoutAPI.requestAccess(
                mediaObject.contentServerId,
                mediaObject.contentId,
                null,
                resolve
            );
        });
    };

    /* ----------------------------- 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
                        contextChange: this.state.contextChange,
                        singleResource: this.state.singleResource,
                        mediaEvents: this.mediaEvents,

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

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

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

                        // annotation & segment related props are stored in the annotation client and propagated via contextChange (for now)
                        annotationClient: this.annotationClient
                    }}
                >
                    {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
};
