import isError from 'lodash/isError';
import React from 'react';
import {v4 as uuidv4} from 'uuid';

import {DEFAULT_DASHBOARDS_CONTEXT, DashboardsContext} from 'src/context';
import {
  DashboardFactory, DummyService, kWebSocketMsgTypeServiceNotification,
  kWebSocketMsgTypeStateNotification, lookupService, lookupServices, sha256,
} from 'src/lib';
import * as WebSocketLib from 'src/lib/websocket';

export class DashboardsProvider extends React.PureComponent {
  constructor (props) {
    super(props);
    this.webSocket = null;
    this.webSocketUnsubscriber = null;
    this.dashboardsHash = null;

    this.state = {
      currentDashboardId: DEFAULT_DASHBOARDS_CONTEXT.currentDashboardId,
      dashboards: DEFAULT_DASHBOARDS_CONTEXT.dashboards,
      isLoading: DEFAULT_DASHBOARDS_CONTEXT.isLoading,
      lastSaveTimestamp: DEFAULT_DASHBOARDS_CONTEXT.lastSaveTimestamp,
      lastSaveError: DEFAULT_DASHBOARDS_CONTEXT.lastSaveError,
      isSaving: DEFAULT_DASHBOARDS_CONTEXT.isSaving,
      isWebSocketConnected: DEFAULT_DASHBOARDS_CONTEXT.isWebSocketConnected,
      loadError: DEFAULT_DASHBOARDS_CONTEXT.loadError,
    };
  }
  handleSaveError = (error) => {
    console.error('Dashboards save error!', error);
    return error.message;
  }
  onWebSocketStart = () => {
    this.setState({isWebSocketConnected: true});
  }
  onWebSocketStop = () => {
    this.setState({isWebSocketConnected: false});
  }
  onWebSocketMessage = async (message) => {
    if (message.type === kWebSocketMsgTypeServiceNotification) {
      if (message.instance) {
        const service = lookupService(
          this.state.dashboards, message.kind, message.instance
        );

        if (!service.isDummy()) {
          service.onMessage(message.data);
        }
      } else {
        const services = lookupServices(this.state.dashboards, message.kind);
        services.forEach((service) => {
          if (!service.isDummy()) {
            service.onMessage(message.data);
          }
        });
      }
    } else if (message.type == kWebSocketMsgTypeStateNotification) {
      const dashboardsHash = await sha256(JSON.stringify(message.data));
      if (this.dashboardsHash !== dashboardsHash) {
        this.setState({
          dashboards: await this.handleLoadedState(message.data),
          lastSaveTimestamp: new Date(),
        });
      }
    }
  }
  currentDashboard = () => {
    return this.state.dashboards.find((dashboard) => {
      return dashboard.id === this.state.currentDashboardId;
    });
  }
  mutateCurrentDashboardServices = (mutator) => {
    return this.state.dashboards.map((dashboard) => {
      if (dashboard.id === this.state.currentDashboardId) {
        return {
          ...dashboard,
          services: mutator(dashboard.services),
        };
      }

      return dashboard;
    });
  }
  serviceFromSpec = (spec) => {
    const ServiceObject = this.props.settings.SERVICES[spec.kind] || DummyService;

    const serviceObject = new ServiceObject({
      ...spec,
      widgetComponent: (
        this.props.settings.WIDGETS[ServiceObject.widget] || null
      ),
    });

    return serviceObject;
  }
  saveDashboards = async (dashboards) => {
    const nextState = {
      isSaving: false,
    };

    try {
      const dashboardsToSave = {
        dashboards: dashboards.map((dashboard) => {
          return {
            ...dashboard,
            services: dashboard.services.map((service) => {
              return service.toJSON();
            }),
          };
        }),
      };

      const dashboardsHash = await sha256(JSON.stringify(dashboardsToSave));
      if (this.dashboardsHash !== dashboardsHash) {
        this.dashboardsHash = dashboardsHash;

        this.setState({
          isSaving: true,
        });

        await this.props.saveDashboards(dashboardsToSave);
        nextState.lastSaveTimestamp =  new Date();
        nextState.lastSaveError = null;
      }
    } catch (error) {
      this.dashboardsHash = null;
      nextState.lastSaveTimestamp =  null;
      nextState.lastSaveError = this.handleSaveError(error);
    }

    this.setState(nextState);
  }
  nukeService = (kind, instance) => {
    const removedService = lookupService(
      this.state.dashboards, kind, instance
    );
    removedService.stop();

    const nextDashboards = this.mutateCurrentDashboardServices((services) => {
      return services.filter((service) => {
        return !(service.kind === kind && service.instance === instance);
      });
    });

    this.saveDashboards(nextDashboards);
    this.setState({
      dashboards: nextDashboards,
    });
  }
  saveServiceCharacteristics = (kind, instance, newCharacteristics) => {
    const updatedService = lookupService(
      this.state.dashboards, kind, instance
    );

    const nextDashboards = this.mutateCurrentDashboardServices((services) => {
      return services.map((service) => {
        if (service === updatedService) {
          service.setCharacteristics(newCharacteristics);
        }

        return service;
      });
    });

    this.saveDashboards(nextDashboards);
    this.setState({
      dashboards: nextDashboards,
    });
  }
  saveServiceLayout = (instance, layout) => {
    const nextDashboards = this.mutateCurrentDashboardServices((services) => {
      return services.map((service) => {
        if (service.instance === instance) {
          service.setLayout(layout);
        }

        return service;
      });
    });

    this.saveDashboards(nextDashboards);
    this.setState({
      dashboards: nextDashboards,
    });
  }
  addService = (kind, characteristics) => {
    const ServiceObject = this.props.settings.SERVICES[kind];
    const WidgetComponent = this.props.settings.WIDGETS[ServiceObject.widget];

    const spec = {
      kind: kind,
      instance: uuidv4(),
      characteristics: characteristics,
    };

    const currentDashboard = this.currentDashboard();
    if (!currentDashboard) {
      return;
    }

    let lastLayout = {y: 0, h: 0};
    currentDashboard.services.forEach((service) => {
      if (service.layout.y >= lastLayout.y) {
        lastLayout = {
          ...lastLayout,
          y: service.layout.y,
        };

        if (service.layout.h > lastLayout.h) {
          lastLayout.h = service.layout.h;
        }
      }
    });

    spec.layout = {
      ...WidgetComponent.defaultLayout || {h: 1, w: 1},
      x: 0,
      y: lastLayout.y + lastLayout.h,
    };

    const nextDashboards = this.mutateCurrentDashboardServices((services) => {
      return [...services, this.serviceFromSpec(spec)];
    });

    this.saveDashboards(nextDashboards);
    this.setState({
      dashboards: nextDashboards,
    });
  }
  handleLoadedState = async (state) => {
    this.dashboardsHash = await sha256(JSON.stringify(state));
    return state.dashboards.map((dashboard) => {
      return {
        ...dashboard,
        services: dashboard.services.map((service) => {
          return this.serviceFromSpec(service);
        }),
      };
    });
  }
  loadDashboards = async () => {
    const nextState = {
      isLoading: false,
    };

    try {
      const state = await this.props.loadDashboards();
      nextState.dashboards = await this.handleLoadedState(state);

      if (this.state.currentDashboardId === null && nextState.dashboards.length > 0) {
        nextState.currentDashboardId = nextState.dashboards[0].id;
      }

      nextState.lastSaveTimestamp = new Date();
    } catch (error) {
      nextState.loadError = this.handleSaveError(error);
    }

    this.setState(nextState);
  }
  setCurrentDashboardId = (dashboardId) => {
    this.setState({
      currentDashboardId: dashboardId,
    });
  }
  addDashboard = (name) => {
    const nextDashboards = [
      ...this.state.dashboards,
      DashboardFactory(name),
    ];

    const nextState = {
      dashboards: nextDashboards,
    };
    if (nextDashboards.length == 1) {
      nextState.currentDashboardId = nextDashboards[0].id;
    }

    this.saveDashboards(nextDashboards);
    this.setState(nextState);
  }
  throwLoadError = (errorOrString) => {
    if (isError(errorOrString)) {
      throw errorOrString;
    }

    throw new Error(errorOrString);
  }
  componentDidMount () {
    this.setState({
      isLoading: true,
    });

    const ConnectorKlass = (
      this.props.settings.CONNECTOR || WebSocketLib.HomeHubWebSocket
    );

    this.webSocket = new ConnectorKlass(
      this.props.settings.DEBUG, this.props.settings.WEBSOCKET
    );
    this.webSocket.addEventListener('start', this.onWebSocketStart);
    this.webSocket.addEventListener('stop', this.onWebSocketStop);
    this.webSocketUnsubscriber = this.webSocket.subscribe(
      this.onWebSocketMessage
    );

    this.loadDashboards();

    if (!this.props.settings.OFFLINE_MODE) {
      this.webSocket.start();
    }
  }
  componentWillUnmount () {
    if (this.webSocketUnsubscriber) {
      this.webSocketUnsubscriber();
      this.webSocketUnsubscriber = null;
    }

    if (this.webSocket) {
      this.webSocket.removeEventListener('start', this.onWebSocketStart);
      this.webSocket.removeEventListener('stop', this.onWebSocketStop);
      this.webSocket.stop();
    }
  }
  render () {
    const contextValue = {
      currentDashboardId: this.state.currentDashboardId,
      dashboards: this.state.dashboards,
      nukeService: this.nukeService,
      saveServiceCharacteristics: this.saveServiceCharacteristics,
      saveServiceLayout: this.saveServiceLayout,
      addService: this.addService,
      isLoading: this.state.isLoading,
      lastSaveTimestamp: this.state.lastSaveTimestamp,
      lastSaveError: this.state.lastSaveError,
      isSaving: this.state.isSaving,
      isWebSocketConnected: this.state.isWebSocketConnected,
      setCurrentDashboardId: this.setCurrentDashboardId,
      dashboardsHash: this.dashboardsHash,
      addDashboard: this.addDashboard,
      loadError: this.state.loadError,
    };

    if (!this.state.isLoading && this.state.loadError !== null) {
      this.throwLoadError(this.state.loadError);
    }

    return (
      <DashboardsContext.Provider value={contextValue}>
        {this.state.isLoading && this.props.loader}
        {!this.state.isLoading && this.props.children}
      </DashboardsContext.Provider>
    );
  }
}
