import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {
  applyMiddleware,
  compose,
  createStore,
  GenericStoreEnhancer,
  Middleware,
  Store,
} from 'redux';

import {
  Provider,
} from 'react-redux';

import {
  ConnectedRouter,
  routerMiddleware,
} from 'react-router-redux';

import createSagaMiddleware, {
  SagaMiddleware,
  SagaIterator,
} from 'redux-saga';

import {
  fork,
} from 'redux-saga/effects';



import {
  createHashHistory,
  History,
} from 'history';

import {
  AppContainer,
} from 'react-hot-loader';


import * as injectTapEventPlugin from 'react-tap-event-plugin';

import Platform from '@overflow/commons/platform';

import WebSocketRPC from '@overflow/commons/websocket/WebSocketRPC';
import ReducerContext from '@overflow/commons/redux/ReducerContext';
import SagaWatcher from '@overflow/commons/redux/saga/SagaWatcher';

import ServiceInvoker from '@overflow/commons/invoke/ServiceInvoker';

import appConfig, { Config, ReduxState } from './config';



import * as AppView from './views/App';


declare global {
  interface Window {
    devToolsExtension: () => any;
  }
  const process: any;
  const module: any;
}

injectTapEventPlugin();

class OFApplication {
  private static isProduction:boolean = process.env.NODE_ENV === 'production' ? true : false;
  private static useReduxDevTools:boolean = window.devToolsExtension && !OFApplication.isProduction ? true : false;

  private config: Config;
  private container: HTMLElement;
  private rpcClient: WebSocketRPC;
  private store: Store<any>;
  private sagaMiddleware: SagaMiddleware<any>;
  private history: History;


  public constructor() {
    this.config = appConfig;
    this.history = createHashHistory();
  }

  public static main(): void {
    let app = new OFApplication();
    app.run();
  }

  public async run(): Promise<void> {
    try {
      this.container = await Platform.getAppContainer(this.config.container.placeholderID);
      this.displayLoading();

      this.rpcClient = await this.initRpcClient();
      // this.context.PouchFactory.registerPouch(this.rpcClient);

      await this.initRedux();

      this.displayApp();
    } catch (e) {
      console.error(e);
      this.displayError(e);
    }
  }

  private initRpcClient(): Promise<WebSocketRPC> {
    const rpcClient = new Promise<WebSocketRPC>((resolve, reject) => {
      let serviceInvoker = new ServiceInvoker(this.config.service);
      let client = new WebSocketRPC(this.config.rpc.url);
      client.initialize()
        .then(() => {
          resolve(client);
        })
        .catch((err: any) => {
          reject(err);
        });
      // resolve(client);
    });

    return rpcClient;
  }

  private initRedux(): Promise<void> {
    const init = new Promise<void>(resolve => {
      // state tree
      // reducer
      for (let reducerMap of this.config.redux.reducerMaps) {
        ReducerContext.putReducers(reducerMap);
      }
      // middleware
      let middlewares: Middleware[] = new Array();
      this.sagaMiddleware = createSagaMiddleware();
      middlewares.push(this.sagaMiddleware);

      let routerReduxMiddleware = routerMiddleware(this.history);
      middlewares.push(routerReduxMiddleware);

      // store
      let middleware: GenericStoreEnhancer = applyMiddleware(...middlewares);
      this.store = createStore(
        ReducerContext.reducer,
        this.config.redux.state,
        OFApplication.useReduxDevTools ? compose(middleware, window.devToolsExtension()) : middleware,
      );
      //  saga
      this.sagaMiddleware.run(this.initReduxSagaWarchers, this.config, this.rpcClient);
      resolve();
    });

    return init;
  }

  private * initReduxSagaWarchers(config: Config, rpcClient: WebSocketRPC): SagaIterator {
    let sagaWatchers: Class<SagaWatcher>[] = config.redux.sagaWatchers;
    for (let type of sagaWatchers) {

      let instance = Object.create(type.prototype);
      instance.constructor.call(instance);
      if (type.prototype.hasOwnProperty('setWebSocketRPC')) {
        instance.setWebSocketRPC.call(instance, rpcClient);
      }
      if (type.prototype.hasOwnProperty('setRestUrl')) {
        instance.setRestUrl.call(instance, config.rest.url);
      }
      yield fork({context: instance, fn: instance.watch});
    }
  }

  private displayLoading(): void {
    ReactDOM.render(
        <div style={{
          width: '100vw',
          height: '100vh',
          backgroundColor: 'white',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          }}>
            <h1>Loading......</h1>
        </div>,
        this.container,
    );
  }

  private displayError(e: Error): void {
    ReactDOM.render(
        <div style={{
          width: '100vw',
          height: '100vh',
          backgroundColor: 'white',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          }}>
            <h1>{e.message}</h1>
        </div>,
        this.container,
    );
  }


  private displayApp(): void {
    OFApplication.isProduction ? this.displayProductionApp() : this.displayDebugApp();
  }
  private displayProductionApp(): void {
    ReactDOM.render(
      <Provider store={this.store}>
        <ConnectedRouter history={this.history}>
          <AppView.default />
        </ConnectedRouter>
      </Provider>,
      this.container,
    );
  }
  private displayDebugApp(): void {
    if (module.hot) {
      module.hot.accept('./views/App', async () => {
        const NextApp = (await import('./views/App')).default;
        ReactDOM.render(
          <AppContainer>
            <Provider store={this.store}>
              <ConnectedRouter history={this.history}>
                <NextApp />
              </ConnectedRouter>
            </Provider>
          </AppContainer>
          ,
          this.container,
        );
      });
    }

    ReactDOM.render(
      <AppContainer>
        <Provider store={this.store}>
          <ConnectedRouter history={this.history}>
            <AppView.default />
          </ConnectedRouter>
        </Provider>
      </AppContainer>
      ,
      this.container,
    );
  }

}

OFApplication.main();
