/**
 * Created by rburson on 4/27/16.
 */

import * as React from 'react'
import {GoogleMap, Marker, InfoWindow, OverlayView, DirectionsRenderer, withGoogleMap} from 'react-google-maps'
import * as SearchBoxMod from 'react-google-maps/lib/places/SearchBox'
const SearchBox = SearchBoxMod.default;

import {
    CvState,
    CvProps,
    CvBaseMixin,
    CvNavigationResult,
    CvEvent,
    CvContext,
    CvValueListener,
    CvStateChangeResult,
    CvQueryPaneCallback,
    CvQueryPane,
    CvValueAdapter,
    CvActionFiredResult,
    CvEventType,
    CvMessage,
    CvMessageType,
    CvValueProvider,
    CvActionHandlerParams,
    CvActionBase
} from 'catreact'
import {
    CvDropdownMenu
} from './catreact-html'
import {FormContext, MapContext, MapDef, ObjUtil, EntityRec, ArrayUtil, Log, DialogException} from 'catavolt-sdk'

export interface CvMapPanelState extends CvState {
}

export interface CvMapPanelProps extends CvProps {
    paneRef?:number;
    formContext?:FormContext;
    mapContext?:MapContext;
    navigationListeners?:Array<(event:CvEvent<CvNavigationResult>)=>void>;
    selectionListener?:CvValueListener<Array<string>>;
    stateChangeListeners?:Array<(event:CvEvent<CvStateChangeResult>)=>void>;
    actionListeners?:Array<(event:CvEvent<CvActionFiredResult>)=>void>
    navTarget?:string;
    containerProps?:{}
    actionProvider?:CvValueProvider<CvActionHandlerParams>;
}

/*
 ***************************************************
 * Render a Map
 ***************************************************
 */
export var CvMapPanel = React.createClass<CvMapPanelProps, CvMapPanelState>({

    mixins: [CvBaseMixin],

    componentDidMount: function () {
        const event:CvEvent<CvMessage> = {type:CvEventType.MESSAGE,
            eventObj:{message: 'Map Tip: Click on multiple markers to see route and distance. Right-click for a context menu.', type:CvMessageType.INFO}}
        this.eventRegistry().publish(event, false);
        CvActionBase._publishActionStarted('#loadMap', this.props.mapContext, true, this.props.actionListeners, this.eventRegistry());
    },

    getDefaultProps: function () {
        return {
            paneRef: null,
            formContext: null,
            mapContext: null,
            navigationListeners: [],
            selectionListener: null,
            stateChangeListeners: [],
            actionListeners: [],
            navTarget: null,
            containerProps: null,
            actionProvider: null
        }
    },

    getInitialState: function () {
        return {currentSelection: null, previousSelection: null, openMarker: null, openMenu: null, directions: null}
    },

    render: function () {


        const queryPaneProps = {
            paneRef: this.props.paneRef,
            formContext: this.props.formContext,
            queryContext: this.props.mapContext,
            stateChangeListeners: this.props.stateChangeListeners,
            actionListeners: this.props.actionListeners,
            actionProvider: this.props.actionProvider
        }

        return <CvQueryPane {...queryPaneProps} queryRenderer={(cvContext:CvContext, callback:CvQueryPaneCallback)=>{

            const mapContext:MapContext = cvContext.scopeCtx.scopeObj;
            const passthroughProps = {
                paneRef: this.props.paneRef,
                formContext:this.props.formContext,
                mapContext: mapContext,
                lastRefreshTime:  mapContext.lastRefreshTime,
                navigationListeners:this.props.navigationListeners,
                selectionListener:this.props.selectionListener,
                stateChangeListeners:this.props.stateChangeListeners,
                actionListeners:this.props.actionListeners,
                navTarget:this.props.navTarget,
                actionProvider:this.props.actionProvider
            }
            const mapContainerProps = ObjUtil.addAllProps(this.props.containerProps, {className: 'cv-component-container cv-map-container'})
            return (
                <CvWrappedMap
                    containerElement={ <div {...mapContainerProps}/> }
                    mapElement={ <div style={{ height: `100%` }} /> }
                    {...passthroughProps}
                />
            );
        }}/>
    }
});

export interface CvMapState extends CvState {
    openMarker:CvMarker;
    currentSelection:CvMarker;
    previousSelection:CvMarker;
    openMenu:{};
    directions:{};
    persistentMarkers:Array<CvMarker>;
    searchMarkers:Array<CvMarker>;
    userMarkers:Array<CvMarker>;

}

export interface CvMapProps extends CvProps {
    paneRef?:number;
    formContext?:FormContext;
    mapContext?:MapContext;
    lastRefreshTime?:Date;
    navigationListeners?:Array<(event:CvEvent<CvNavigationResult>)=>void>;
    selectionListener?:CvValueListener<Array<string>>;
    stateChangeListeners?:Array<(event:CvEvent<CvStateChangeResult>)=>void>;
    actionListeners?:Array<(event:CvEvent<CvActionFiredResult>)=>void>
    navTarget?:string;
    actionProvider?:CvValueProvider<CvActionHandlerParams>;
}

export interface CvMarker {
    id:string;
    lat:number;
    lng:number;
    desc:string;
    imgUrl?: string;
    tipText?:string;
    imagePlc?:string;
    draggable?:boolean;
}

const CvMapPanel_INPUT_STYLE = {
    boxSizing: `border-box`,
    MozBoxSizing: `border-box`,
    border: `1px solid transparent`,
    width: `240px`,
    height: `32px`,
    marginTop: `27px`,
    padding: `0 12px`,
    borderRadius: `1px`,
    boxShadow: `0 2px 6px rgba(0, 0, 0, 0.3)`,
    fontSize: `14px`,
    outline: `none`,
    textOverflow: `ellipses`,
};


const CvMap = React.createClass<CvMapProps, CvMapState>({

    mixins: [CvBaseMixin],

    componentWillMount: function() {
        this.setState({persistentMarkers: this._generatePersistentMarkers()});
    },

    componentWillReceiveProps: function(nextProps, nextState) {
        if(nextProps.lastRefreshTime && (nextProps.lastRefreshTime.getTime() > this.props.lastRefreshTime.getTime())){
            this.setState({persistentMarkers: this._generatePersistentMarkers()});
        }
    },

    getDefaultProps: function () {
        return {
            paneRef: null,
            formContext: null,
            mapContext: null,
            lastRefreshTime: null,
            navigationListeners: [],
            selectionListener: null,
            stateChangeListeners: [],
            actionListeners: [],
            navTarget: null,
            actionProvider: null
        }
    },

    getInitialState: function () {
        return {currentSelection: null, previousSelection: null, openMarker: null, openMenu: null, directions: null, persistentMarkers: [], searchMarkers:[], userMarkers:[]}
    },

    render: function () {

        const {currentSelection, previousSelection, directions, openMenu, openMarker} = this.state;
        const mapContext = this.props.mapContext;
        const markers:Array<CvMarker> = this._getAllMarkers();
        return (
               <GoogleMap
                    defaultZoom={7}
                    defaultCenter={{lat: 39.144973, lng: -100.019531}}
                    defaultOptions={{fullscreenControl: true, disableDoubleClickZoom: true}}
                    onClick={this._handleMapClick}
                    onDblClick={this._handleMapDoubleClick}
                    ref={(map)=>{if(map){ this._initMap(map, markers)}}}>

                    <SearchBox
                        controlPosition={google.maps.ControlPosition.TOP_LEFT}
                        onPlacesChanged={this._onPlacesChanged}
                        inputPlaceholder="Search Places"
                        inputStyle={CvMapPanel_INPUT_STYLE}
                        bounds={this._gMap ? this._gMap.getBounds() : null}
                        ref={ref=>this._searchBox = ref}
                    />

                    {directions ? <DirectionsRenderer directions={directions}
                                                      options={{suppressMarkers:true}}/> : null}

                    {markers.map((marker:CvMarker, index:number)=>{

                        const menuDef = (mapContext.menuDefs && mapContext.menuDefs.length > 0) ? mapContext.menuDefs[0].findContextMenuDef() : null;
                        const selectionAdapter:CvValueAdapter<Array<string>> = new CvValueAdapter<Array<string>>();
                        selectionAdapter.getDelegateValueListener()([marker.id]);
                        /*
                         Note:
                         We have to explicitly pass every prop to the CvDropdownMenu component that might normally be found in the context
                         because the 'GoogleMap' react library interferes with the context
                         */
                        return (
                            <Marker
                                defaultPosition={{lat: marker.lat, lng: marker.lng}}
                                defaultAnimation={google.maps.Animation.DROP}
                                key={marker.id}
                                icon={marker.imgUrl ? {url:marker.imgUrl, scaledSize:new google.maps.Size(30, 30)} : null}
                                title={marker.tipText}
                                draggable={marker.draggable}
                                onMouseOver={this._handleMarkerOver.bind(this, marker)}
                                onClick={this._handleMarkerClick.bind(this, marker)}
                                onRightClick={this._handleMarkerRightClick.bind(this, marker)}
                                onDblClick={this._handleMarkerDoubleClick.bind(this, marker)}
                                onDragEnd={this._handleDragEnd.bind(this, marker)}>

                                {openMarker && openMarker.id === marker.id ?
                                    this._getInfoDisplay(directions, marker, currentSelection) : null}

                                {openMenu && openMenu.id === marker.id ?
                                    <OverlayView
                                        position={{ lat: marker.lat, lng: marker.lng }}
                                        mapPaneName={OverlayView.OVERLAY_MOUSE_TARGET}
                                        getPixelPositionOffset={this._getPixelPositionOffset}>
                                        <CvDropdownMenu paneContext={mapContext}
                                                        menuDef={menuDef}
                                                        navigationListeners={this.props.navigationListeners}
                                                        actionListeners={[this._actionFired.bind(this, marker)]}
                                                        stateChangeListeners={this.props.stateChangeListeners}
                                                        navTarget={this.props.navTarget}
                                                        selectionProvider={selectionAdapter}
                                                        catavolt={this.catavolt()}
                                                        initOpen={true}
                                                        launchMenuElement={<span/>}
                                                        eventRegistry={this.eventRegistry()}/>
                                    </OverlayView>
                                    : null}
                            </Marker>
                        );
                    })}
                </GoogleMap>
        );
    },

    _actionFired: function (marker:any, event:CvEvent<CvActionFiredResult>) {
    },

    _getAllMarkers: function():Array<CvMarker> {
        return [...this.state.persistentMarkers, ...this.state.searchMarkers, ...this.state.userMarkers];
    },

    /*
        Use info window to display directions
     */
    _getInfoDisplay: function(directions, marker:CvMarker, targetMarker:CvMarker) {

        if(!directions) {
            return <InfoWindow onCloseClick={this._handleInfoClose.bind(this, marker)}><div>{marker.desc}</div></InfoWindow>
        } else {
            if(marker.id == targetMarker.id) {

                const leg = directions.routes[0].legs[0];
                return <InfoWindow onCloseClick={this._handleInfoClose.bind(this, marker)}>
                    <div>
                        <span className="cv-icon cv-map-info pull-right" aria-hidden="true"/>
                        <div><strong>{'To: '}</strong>{marker.desc}</div>
                        <div>{leg.end_address}</div>
                        <div>{leg.distance ? leg.distance.text : null}</div>
                        <div>{leg.duration ? leg.duration.text : null}</div>
                    </div>
                </InfoWindow>
            }
        }

        return null;

    },

    /*
        Create the markers that are included in the ListContext
     */
    _generatePersistentMarkers: function():Array<CvMarker> {

        const mapContext = this.props.mapContext;
        const mapDef:MapDef = mapContext.mapDef;
        const records:Array<EntityRec> = ArrayUtil.copy<EntityRec>(mapContext.scroller.buffer);
        const markers:Array<CvMarker> = records.map((record:EntityRec)=>{
            return {
                id: record.objectId,
                lat: record.propAtName(mapDef.latitudePropName).value,
                lng: record.propAtName(mapDef.longitudePropName).value,
                desc: record.propAtName(mapDef.descriptionPropName).value,
                imgUrl: record.imageName,
                tipText: record.tipText,
                imagePlc: record.imagePlacement
            }
        });

        return markers;
    },


    _getPixelPositionOffset: function(width, height) {
        return { x: -(width / 2), y: -(height / 2) };
    },

    /*
        Update the position and info of the User-placed Marker, when dragged
     */
    _handleDragEnd: function(marker:CvMarker, e) {

        const currentUserMarkers = this.state.userMarkers;
        currentUserMarkers.splice(currentUserMarkers.indexOf(marker), 1);
        const nextMarker = ObjUtil.addAllProps(marker, {});
        const lat = e.latLng.lat();
        const lng = e.latLng.lng();
        nextMarker.lat = lat;
        nextMarker.lng = lng;
        nextMarker.desc = lat + ', ' + lng;
        currentUserMarkers.push(nextMarker);
        this._reverseGeolocate(lat, lng, ((address)=>{
            nextMarker.desc = address;
            this.setState({userMarkers: currentUserMarkers});
        }));

    },

    /*
        Clicking on an empty area of the map should reset selected markers, directions, etc.
     */
    _handleMapClick: function(e) {
        if(!this.state.openMenu) {
            this.setState({
                openMarker: null,
                directions: null,
                currentSelection: null,
                previousSelection: null
            });
        }
    },

    /*
        Place a User Marker, where double-clicked
     */
    _handleMapDoubleClick: function(e) {

        const lat = e.latLng.lat();
        const lng = e.latLng.lng();

        const userMarkers:Array<CvMarker> = [...this.state.userMarkers];
        const marker = {
                id: 'user_' + userMarkers.length,
                lat: lat,
                lng: lng,
                desc: 'Dropped Pin',
                tipText: lat + ', ' + lng,
                draggable: true
        }

        this._reverseGeolocate(lat, lng, (address=>marker.desc = address));
        userMarkers.push(marker);
        this.setState({userMarkers: userMarkers});
    },

    /*
        Remove a User Marker when double-clicked
     */
    _handleMarkerDoubleClick: function(marker:CvMarker, e) {

        const currentUserMarkers = this.state.userMarkers;
        currentUserMarkers.splice(currentUserMarkers.indexOf(marker), 1);
        this.setState({
            userMarkers: currentUserMarkers,
            openMarker: null,
            directions: null,
            currentSelection: null,
            previousSelection: null
        });

    },

    /*
        Show marker info when moused over
     */
    _handleMarkerOver: function (marker, e) {
        if(!this.state.directions) {
            this.setState({
                openMarker: marker,
                openMenu: null
            });
        }
    },

    /*
        When marker is clicked, make it the current selection and check for a previously clicked marker
        from which to route directions
     */
    _handleMarkerClick: function (marker, e) {

        if (this.props.selectionListener) this.props.selectionListener([marker.id]);
        const previousSelection = this.state.currentSelection;
        const currentSelection = marker;

        if(previousSelection && currentSelection.id !== previousSelection.id) {

            const ds = new google.maps.DirectionsService();
            ds.route({
                origin: new google.maps.LatLng(previousSelection.lat, previousSelection.lng),
                destination: new google.maps.LatLng(currentSelection.lat, currentSelection.lng),
                travelMode: google.maps.TravelMode.DRIVING,
                provideRouteAlternatives: true
            }, (result, status) => {
                if (status === google.maps.DirectionsStatus.OK) {
                    this.setState({
                        directions: result,
                        currentSelection: currentSelection,
                        previousSelection: previousSelection,
                        openMarker: marker,
                        openMenu: null
                    });
                } else {
                    const event:CvEvent<CvMessage> = {type:CvEventType.MESSAGE,
                        eventObj:{message: `error fetching directions ${ result }`,
                            messageObj:new DialogException('', 'Error retrieving directions'), type:CvMessageType.ERROR}}
                    this.eventRegistry().publish(event, false);
                }
            });
        } else {
            this.setState({
                currentSelection: currentSelection,
                previousSelection: previousSelection,
                openMenu: null,
                directions: null
            });
        }

    },

    /*
        Show context menu when a Persistent Marker is right-clicked
     */
    _handleMarkerRightClick: function(marker, e) {
        if (this.props.selectionListener) this.props.selectionListener([marker.id]);
        this.setState({
            previousSelection: this.state.currentSelection,
            currentSelection: marker,
            openMenu: marker
        });
    },

    /*
        Close the info window
     */
    _handleInfoClose: function (marker, e) {
        this.setState({openMarker: null});
    },

    /*
        Initialize the map and set bounds
     */
    _initMap: function (map:any, latLng:Array<{lat; lng;}>) {
        if (!this._gMap && latLng && latLng.length > 0) {
            this._gMap = map;
            this._setBounds(map, latLng);
            CvActionBase._publishActionFinished('#loadMap', this.props.mapContext, this.props.actionListeners, this.eventRegistry());
        }
    },

    _setBounds: function(map:any, latLng:Array<{lat; lng;}>) {
        const latLngBounds = new google.maps.LatLngBounds();
        for (var i = 0; i < latLng.length; i++) {
            latLngBounds.extend(new google.maps.LatLng(latLng[i].lat, latLng[i].lng));
        }
        map.fitBounds(latLngBounds);
    },

    _geoLocate: function(callback:(pos:{lat; lng;})=>void) {
        if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(function(position) {
                var pos = {
                    lat: position.coords.latitude,
                    lng: position.coords.longitude
                };
                callback(pos);
            }, function() {
                const event:CvEvent<CvMessage> = {type:CvEventType.MESSAGE,
                    eventObj:{message: 'Geolocation failed',
                        messageObj:new DialogException('', 'Geolocation failed'), type:CvMessageType.ERROR}}
                this.eventRegistry().publish(event, false);
            });
        } else {
            const event:CvEvent<CvMessage> = {type:CvEventType.MESSAGE,
                eventObj:{message: 'Geolocation not supported (or disabled)',
                    messageObj:new DialogException('', 'Geolocation not supported (or disabled)'), type:CvMessageType.ERROR}}
            this.eventRegistry().publish(event, false);
        }
    },

    /*
        Handle search results
     */
    _onPlacesChanged: function() {

        const markers = [];
        this._searchBox.getPlaces().forEach((place)=>{
            markers.push({
                id: place.place_id,
                lat: place.geometry.location.lat(),
                lng: place.geometry.location.lng(),
                desc: place.formatted_address,
                imgUrl: place.icon,
                tipText: place.name,
            });
        });

        this._setBounds(this._gMap, [...markers, ...this.state.userMarkers, ...this.state.persistentMarkers]);
        this.setState({searchMarkers: markers});
    },

    _reverseGeolocate: function(lat, lng, callback:(address:string)=>void) {

        var geocoder = new google.maps.Geocoder;
        geocoder.geocode({'location': {lat: lat, lng: lng}}, (results, status) => {
            if (status == google.maps.GeocoderStatus.OK) {
                if (results[0]) {
                    callback(results[0].formatted_address)
                }
            } else {
                const event:CvEvent<CvMessage> = {type:CvEventType.MESSAGE,
                    eventObj:{message: 'Reverse Geolocation failed',
                        messageObj:new DialogException('', 'Reverse Geolocation failed'), type:CvMessageType.ERROR}}
                this.eventRegistry().publish(event, false);
            }
        });
    }
});

const CvWrappedMap = withGoogleMap(props => (<CvMap {...props}/>));

