import { analyticsService, AnalyticsHandler } from '../../analytics';
import { EditorState, EditorView, Node, Schema, MarkSpec, Plugin, Transaction } from '../../prosemirror';
import { EditorInstance, EditorPlugin, EditorProps, EditorConfig } from '../types';
import ProviderFactory from '../../providerFactory';
import ErrorReporter from '../../utils/error-reporter';
import { EventDispatcher, createDispatch, Dispatch } from '../event-dispatcher';

export function sortByRank(a: { rank: number }, b: { rank: number }): number {
  return a.rank - b.rank;
}

export function fixExcludes(marks: { [key: string]: MarkSpec }): { [key: string]: MarkSpec } {
  const markKeys = Object.keys(marks);
  markKeys.map(markKey => {
    const mark = marks[markKey];
    if (mark.excludes) {
      mark.excludes = mark.excludes
        .split(' ')
        .filter(exMarkKey => markKeys.indexOf(exMarkKey) > -1)
        .join(' ');
    }
  });
  return marks;
}

export function processPluginsList(plugins: EditorPlugin[]): EditorConfig {
  return plugins.reduce(
    (acc, plugin) => {
      if (plugin.pmPlugins) {
        acc.pmPlugins.push(...plugin.pmPlugins());
      }

      if (plugin.nodes) {
        acc.nodes.push(...plugin.nodes());
      }

      if (plugin.marks) {
        acc.marks.push(...plugin.marks());
      }

      if (plugin.contentComponent) {
        acc.contentComponents.push(plugin.contentComponent);
      }

      if (plugin.primaryToolbarComponent) {
        acc.primaryToolbarComponents.push(plugin.primaryToolbarComponent);
      }

      if (plugin.secondaryToolbarComponent) {
        acc.secondaryToolbarComponents.push(plugin.secondaryToolbarComponent);
      }

      return acc;
    },
    {
      nodes: [],
      marks: [],
      pmPlugins: [],
      contentComponents: [],
      primaryToolbarComponents: [],
      secondaryToolbarComponents: []
    } as EditorConfig
  );
}

export function createSchema(editorConfig: EditorConfig) {
  const nodes = editorConfig.nodes.sort(sortByRank).reduce((acc, node) => {
    acc[node.name] = node.node;
    return acc;
  }, {});

  const marks = fixExcludes(
    editorConfig.marks.sort(sortByRank).reduce((acc, mark) => {
      acc[mark.name] = mark.mark;
      return acc;
    }, {})
  );

  return new Schema({ nodes, marks });
}

export function createPMPlugins(
  editorConfig: EditorConfig,
  schema: Schema<any, any>,
  props: EditorProps,
  dispatch: Dispatch,
  providerFactory: ProviderFactory,
  errorReporter: ErrorReporter
): Plugin[] {
  return editorConfig.pmPlugins
    .sort(sortByRank)
    .map(({ plugin }) => plugin(schema, props, dispatch, providerFactory, errorReporter))
    .filter(plugin => !!plugin) as Plugin[];
}

export function createErrorReporter(errorReporterHandler) {
  const errorReporter = new ErrorReporter();
  if (errorReporterHandler) {
    errorReporter.handler = errorReporterHandler;
  }
  return errorReporter;
}

export function initAnalytics(analyticsHandler?: AnalyticsHandler) {
  analyticsService.handler = analyticsHandler || (() => {});
  analyticsService.trackEvent('atlassian.editor.start');
}

export function processDefaultDocument(schema: Schema<any, any>, rawDoc?: Node | string | Object): Node | undefined {
  if (!rawDoc) {
    return;
  }

  if (rawDoc instanceof Node) {
    return rawDoc;
  }

  let doc: Object;
  if (typeof rawDoc === 'string') {
    try {
      doc = JSON.parse(rawDoc);
    } catch (e) {
      console.error(`Error processing default value: ${rawDoc} isn't valid JSON document`);
      return;
    }
  } else {
    doc = rawDoc;
  }

  if (Array.isArray(doc)) {
    console.error(`Error processing default value: ${doc} is an array, but it must be an object with the following shape { type: 'doc', content: [...] }`);
    return;
  }

  try {
    return Node.fromJSON(schema, doc);
  } catch (e) {
    console.error(`Error processing default value: ${doc} – ${e.message}`);
    return;
  }
}

/**
 * Creates and mounts EditorView to the provided place.
 */
export default function createEditor(
  place: HTMLElement | null,
  editorPlugins: EditorPlugin[] = [],
  props: EditorProps,
  providerFactory: ProviderFactory
): EditorInstance {
  const editorConfig = processPluginsList(editorPlugins);
  const { contentComponents, primaryToolbarComponents, secondaryToolbarComponents } = editorConfig;
  const { contentTransformerProvider, defaultValue } = props;

  initAnalytics(props.analyticsHandler);

  const errorReporter = createErrorReporter(props.errorReporterHandler);
  const eventDispatcher = new EventDispatcher();
  const dispatch = createDispatch(eventDispatcher);
  const schema = createSchema(editorConfig);
  const plugins = createPMPlugins(editorConfig, schema, props, dispatch, providerFactory, errorReporter);
  const contentTransformer = contentTransformerProvider ? contentTransformerProvider(schema) : undefined;
  const doc = (contentTransformer && typeof defaultValue === 'string')
    ? contentTransformer.parse(defaultValue)
    : processDefaultDocument(schema, defaultValue);

  const state = EditorState.create({ doc, schema, plugins });
  const editorView = new EditorView(place, {
    state,
    dispatchTransaction(tr: Transaction) {
      tr.setMeta('isLocal', true);
      const newState = editorView.state.apply(tr);
      editorView.updateState(newState);
    }
  });

  return {
    editorView,
    eventDispatcher,
    contentComponents,
    primaryToolbarComponents,
    secondaryToolbarComponents,
    contentTransformer
  };
}
