import { isNumber } from 'lodash';
import Feature, { type FeatureLike } from 'ol/Feature';
import type OpenLayersMap from 'ol/Map';
import { type Geometry, LineString, type Point, SimpleGeometry } from 'ol/geom';
import VectorImage from 'ol/layer/VectorImage';
import { Fill, Stroke, Style, Text } from 'ol/style';
import FlowLine from 'ol-ext/style/FlowLine';
import { type ReactNode } from 'react';
import { ReplaySubject } from 'rxjs';
import tinycolor from 'tinycolor2';

import {
  type MapLayerRegistryItem,
  type MapLayerOptions,
  type PanelData,
  type GrafanaTheme2,
  FrameGeometrySourceMode,
  type EventBus,
  type DataFrame,
  type Field,
} from '@grafana/data';
import { TextDimensionMode } from '@grafana/schema';
import { FrameVectorSource } from 'app/features/geo/utils/frameVectorSource';
import { getGeometryField, getLocationMatchers } from 'app/features/geo/utils/location';
import { type GraphFrame } from 'app/plugins/panel/nodeGraph/types';
import { getGraphFrame } from 'app/plugins/panel/nodeGraph/utils';

import { type MarkersLegendProps, MarkersLegend } from '../../components/MarkersLegend';
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
import { StyleEditor } from '../../editor/StyleEditor';
import { type StyleConfig, defaultStyleConfig } from '../../style/types';
import { getStyleConfigState } from '../../style/utils';
import { getStyleDimension } from '../../utils/utils';

export interface NetworkConfig {
  style: StyleConfig;
  showLegend?: boolean;
  edgeStyle: StyleConfig;
  arrow?: 0 | 1 | -1 | 2;
}

const defaultOptions: NetworkConfig = {
  style: defaultStyleConfig,
  showLegend: false,
  edgeStyle: defaultStyleConfig,
  arrow: 0,
};

export const NETWORK_LAYER_ID = 'network';

// Used by default when nothing is configured
export const defaultMarkersConfig: MapLayerOptions<NetworkConfig> = {
  type: NETWORK_LAYER_ID,
  name: '', // will get replaced
  config: defaultOptions,
  location: {
    mode: FrameGeometrySourceMode.Auto,
  },
};

/**
 * Map layer configuration for network overlay
 */
export const networkLayer: MapLayerRegistryItem<NetworkConfig> = {
  id: NETWORK_LAYER_ID,
  name: 'Network',
  description: 'Render a node graph as a map layer',
  isBaseMap: false,
  showLocation: true,
  hideOpacity: true,

  /**
   * Function that configures transformation and returns a transformer
   * @param map
   * @param options
   * @param theme
   */
  create: async (map: OpenLayersMap, options: MapLayerOptions<NetworkConfig>, eventBus: EventBus, theme: GrafanaTheme2) => {
    // Assert default values
    const config = {
      ...defaultOptions,
      ...options?.config,
    };

    const style = await getStyleConfigState(config.style);
    const edgeStyle = await getStyleConfigState(config.edgeStyle);
    const location = await getLocationMatchers(options.location);
    const source = new FrameVectorSource(location);
    const vectorLayer = new VectorImage({
      source
    });
    const hasArrows = config.arrow === 1 || config.arrow === -1 || config.arrow === 2;

    // TODO update legend to display edges as well
    const legendProps = new ReplaySubject<MarkersLegendProps>(1);
    let legend: ReactNode = null;
    if (config.showLegend) {
      legend = <ObservablePropsWrapper watch={legendProps} initialSubProps={{}} child={MarkersLegend} />;
    }

    vectorLayer.setStyle((feature: FeatureLike) => {
      const geom = feature.getGeometry();
      const idx = feature.get('rowIndex');
      const dims = style.dims;

      if (!style.fields && !edgeStyle.fields && !hasArrows && geom?.getType() !== 'LineString') {
        // Set a global style
        return style.maker(style.base);
      }

      // For edges
      if (geom?.getType() === 'LineString' && geom instanceof SimpleGeometry) {
        const edgeDims = edgeStyle.dims;
        const edgeTextConfig = edgeStyle.config.textConfig;
        const edgeId = Number(feature.getId());
        const coordinates = geom.getCoordinates();
        const opacity = edgeStyle.config.opacity ?? 1;
        if (coordinates && edgeDims) {
          const segmentStartCoords = coordinates[0];
          const segmentEndCoords = coordinates[1];
          const color1 = tinycolor(
            theme.visualization.getColorByName((edgeDims.color && edgeDims.color.get(edgeId)) ?? edgeStyle.base.color)
          )
            .setAlpha(opacity)
            .toString();
          const color2 = tinycolor(
            theme.visualization.getColorByName((edgeDims.color && edgeDims.color.get(edgeId)) ?? edgeStyle.base.color)
          )
            .setAlpha(opacity)
            .toString();
          const arrowSize1 = (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size;
          const arrowSize2 = (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size;
          const styles = [];

          const flowStyle = new FlowLine({
            visible: true,
            lineCap: config.arrow === 0 ? 'round' : 'square',
            color: color1,
            color2: color2,
            width: (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size,
            width2: (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size,
          });

          if (config.arrow) {
            flowStyle.setArrow(config.arrow);
            if (config.arrow > 0) {
              flowStyle.setArrowColor(color2);
              flowStyle.setArrowSize((arrowSize2 ?? 0) * 2);
            } else {
              flowStyle.setArrowColor(color1);
              flowStyle.setArrowSize((arrowSize1 ?? 0) * 2);
            }
          }
          const LS = new LineString([segmentStartCoords, segmentEndCoords]);
          flowStyle.setGeometry(LS);

          const fontFamily = theme.typography.fontFamily;
          if (edgeDims.text || edgeStyle.config.text?.mode === TextDimensionMode.Fixed) {
            const labelStyle = new Style({
              zIndex: 10,
              text: new Text({
                text: edgeDims.text?.get(edgeId) ?? edgeStyle.config.text?.fixed,
                font: `normal ${edgeTextConfig?.fontSize}px ${fontFamily}`,
                fill: new Fill({ color: color1 ?? defaultStyleConfig.color.fixed }),
                stroke: new Stroke({
                  color: tinycolor(theme.visualization.getColorByName('text')).setAlpha(opacity).toString(),
                  width: Math.max(edgeTextConfig?.fontSize! / 10, 1),
                }),
                ...edgeTextConfig,
              }),
            });
            labelStyle.setGeometry(LS);
            styles.push(labelStyle);
          }
          styles.push(flowStyle);
          return styles;
        }
      }
      if (!dims || !isNumber(idx)) {
        return style.maker(style.base);
      }

      const values = { ...style.base };

      if (dims.color) {
        values.color = dims.color.get(idx);
      }
      if (dims.size) {
        values.size = dims.size.get(idx);
      }
      if (dims.text) {
        values.text = dims.text.get(idx);
      }
      if (dims.rotation) {
        values.rotation = dims.rotation.get(idx);
      }
      return style.maker(values);
    });

    return {
      init: () => vectorLayer,
      legend: legend,
      update: (data: PanelData) => {
        if (!data.series?.length) {
          source.clear();
          return; // ignore empty
        }

        // Post updates to the legend component
        if (legend) {
          legendProps.next({
            styleConfig: style,
            size: style.dims?.size,
            layerName: options.name,
            layer: vectorLayer,
          });
        }
        const graphFrames = getGraphFrame(data.series);

        for (const frame of data.series) {
          if (frame === graphFrames.edges[0]) {
            edgeStyle.dims = getStyleDimension(frame, edgeStyle, theme);
          } else {
            style.dims = getStyleDimension(frame, style, theme);
          }

          updateEdge(source, graphFrames);
        }
      },

      // Marker overlay options
      registerOptionsUI: (builder, context) => {
        const networkFrames = getGraphFrame(context.data);
        const frameNodes = networkFrames.nodes[0];
        const frameEdges = networkFrames.edges[0];

        builder
          .addCustomEditor({
            id: 'config.style',
            category: ['Node Styles'],
            path: 'config.style',
            name: 'Node Styles',
            editor: StyleEditor,
            settings: {
              displayRotation: true,
              frameMatcher: (frame: DataFrame) => frame === frameNodes,
            },
            defaultValue: defaultOptions.style,
          })
          .addCustomEditor({
            id: 'config.edgeStyle',
            category: ['Edge Styles'],
            path: 'config.edgeStyle',
            name: 'Edge Styles',
            editor: StyleEditor,
            settings: {
              hideSymbol: true,
              frameMatcher: (frame: DataFrame) => frame === frameEdges,
            },
            defaultValue: defaultOptions.style,
          })
          .addRadio({
            path: 'config.arrow',
            name: 'Arrow',
            settings: {
              options: [
                { label: 'None', value: 0 },
                { label: 'Forward', value: 1 },
                { label: 'Reverse', value: -1 },
                { label: 'Both', value: 2 },
              ],
            },
            defaultValue: defaultOptions.arrow,
          })
          .addBooleanSwitch({
            path: 'config.showLegend',
            name: 'Show legend',
            description: 'Show map legend',
            defaultValue: defaultOptions.showLegend,
          });
      },
    };
  },

  // fill in the default values
  defaultOptions,
};

function updateEdge(source: FrameVectorSource, graphFrames: GraphFrame) {
  source.clear(true);

  const frameNodes = graphFrames.nodes[0];
  const frameEdges = graphFrames.edges[0];

  if (!frameNodes || !frameEdges) {
    // TODO: provide helpful error message / link to docs for how to format data
    return;
  }

  const info = getGeometryField(frameNodes, source.location);
  if (!info.field) {
    source.changed();
    return;
  }

  // TODO: Fix this
  // eslint-disable-next-line
  const field = info.field as unknown as Field<Point>;

  // TODO for nodes, don't hard code id field name
  const nodeIdIndex = frameNodes.fields.findIndex((f: Field) => f.name === 'id');
  const nodeIdValues = frameNodes.fields[nodeIdIndex].values;

  // Edges
  // TODO for edges, don't hard code source and target fields
  const sourceIndex = frameEdges.fields.findIndex((f: Field) => f.name === 'source');
  const targetIndex = frameEdges.fields.findIndex((f: Field) => f.name === 'target');

  const sources = frameEdges.fields[sourceIndex].values;
  const targets = frameEdges.fields[targetIndex].values;

  // Loop through edges, referencing node locations
  for (let i = 0; i < sources.length; i++) {
    // Create linestring for each edge
    const sourceId = sources[i];
    const targetId = targets[i];

    const sourceNodeIndex = nodeIdValues.findIndex((value: string) => value === sourceId);
    const targetNodeIndex = nodeIdValues.findIndex((value: string) => value === targetId);

    if (!field.values[sourceNodeIndex] || !field.values[targetNodeIndex]) {
      continue;
    }

    const geometryEdge: Geometry = new LineString([
      field.values[sourceNodeIndex].getCoordinates(),
      field.values[targetNodeIndex].getCoordinates(),
    ]);

    const edgeFeature = new Feature({
      geometry: geometryEdge,
    });
    edgeFeature.setId(i);
    source['addFeatureInternal'](edgeFeature); // @TODO revisit?
  }

  // Nodes
  for (let i = 0; i < frameNodes.length; i++) {
    source['addFeatureInternal'](
      new Feature({
        frameNodes,
        rowIndex: i,
        geometry: info.field.values[i],
      })
    );
  }

  // only call source at the end
  source.changed();
}
