/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { PureComponent } from 'react';
import cloudLayout from 'd3-cloud';
import { scaleLinear } from 'd3-scale';
import { seed, CategoricalColorNamespace } from '@superset-ui/core';
import { SupersetTheme, withTheme } from '@apache-superset/core/theme';
import { isEqual } from 'lodash';

const seedRandom = seed('superset-ui');

export type PlainObject = Record<string, unknown>;

// Polyfill Word type since it's not exported from 'd3-cloud'
export type Word = {
  text: string;
  size: number;
  x?: number;
  y?: number;
  rotate?: number;
  font?: string;
  weight?: string | number;
};

export const ROTATION = {
  flat: () => 0,
  random: () => Math.floor(seedRandom() * 6 - 3) * 30,
  square: () => Math.floor(seedRandom() * 2) * 90,
};

export type RotationType = keyof typeof ROTATION;

/**
 * Encoding configuration for mapping data fields to visual properties.
 * Supports field-based mappings with optional scale configurations.
 */
export interface WordCloudEncoding {
  color?: {
    field?: string;
    value?: string;
    scale?: { scheme?: string };
    type?: string;
  };
  fontFamily?: { field?: string; value?: string };
  fontSize?: {
    field?: string;
    value?: number;
    scale?: { range?: [number, number]; zero?: boolean };
    type?: string;
  };
  fontWeight?: { field?: string; value?: string | number };
  text?: { field?: string; value?: string };
}

export interface WordCloudVisualProps {
  encoding?: Partial<WordCloudEncoding>;
  rotation?: RotationType;
}

export interface WordCloudProps extends WordCloudVisualProps {
  data: PlainObject[];
  height: number;
  width: number;
  sliceId: number;
  colorScheme: string;
}

export interface WordCloudState {
  words: Word[];
  scaleFactor: number;
}

const defaultProps: Required<WordCloudVisualProps> = {
  encoding: {},
  rotation: 'flat',
};

type FullWordCloudProps = WordCloudProps &
  typeof defaultProps & { theme: SupersetTheme };

const SCALE_FACTOR_STEP = 0.5;
const MAX_SCALE_FACTOR = 3;
const TOP_RESULTS_PERCENTAGE = 0.1;

/**
 * Simple encoder that maps data fields to visual properties.
 * Replaces the encodable library with direct field access and d3 scales.
 */
class SimpleEncoder {
  private encoding: WordCloudEncoding;

  private defaults: {
    color: string;
    fontFamily: string;
    fontSize: number;
    fontWeight: string | number;
    text: string;
  };

  private fontSizeScale: ((value: number) => number) | null = null;

  constructor(
    encoding: WordCloudEncoding,
    defaults: {
      color: string;
      fontFamily: string;
      fontSize: number;
      fontWeight: string | number;
      text: string;
    },
  ) {
    this.encoding = encoding;
    this.defaults = defaults;
  }

  /**
   * Set domain from dataset to configure scales
   */
  setDomainFromDataset(data: PlainObject[]): void {
    const fontSizeConfig = this.encoding.fontSize;
    if (fontSizeConfig?.field && fontSizeConfig?.scale?.range) {
      const values = data
        .map(d => Number(d[fontSizeConfig.field!]) || 0)
        .filter(v => !Number.isNaN(v));

      if (values.length > 0) {
        const min = fontSizeConfig.scale.zero ? 0 : Math.min(...values);
        const max = Math.max(...values);
        const [rangeMin, rangeMax] = fontSizeConfig.scale.range;

        this.fontSizeScale = scaleLinear()
          .domain([min, max])
          .range([rangeMin, rangeMax]);
      }
    }
  }

  getText(d: PlainObject): string {
    const config = this.encoding.text;
    if (config?.field && d[config.field] !== undefined) {
      return String(d[config.field]);
    }
    return config?.value ?? this.defaults.text;
  }

  getFontSize(d: PlainObject): number {
    const config = this.encoding.fontSize;
    if (config?.field && d[config.field] !== undefined) {
      const value = Number(d[config.field]) || 0;
      if (this.fontSizeScale) {
        return this.fontSizeScale(value);
      }
      return value || this.defaults.fontSize;
    }
    return config?.value ?? this.defaults.fontSize;
  }

  getColor(d: PlainObject): string {
    const config = this.encoding.color;
    if (config?.field && d[config.field] !== undefined) {
      return String(d[config.field]);
    }
    return config?.value ?? this.defaults.color;
  }

  getFontFamily(d: PlainObject): string {
    const config = this.encoding.fontFamily;
    if (config?.field && d[config.field] !== undefined) {
      return String(d[config.field]);
    }
    return config?.value ?? this.defaults.fontFamily;
  }

  getFontWeight(d: PlainObject): string | number {
    const config = this.encoding.fontWeight;
    if (config?.field && d[config.field] !== undefined) {
      return d[config.field] as string | number;
    }
    return config?.value ?? this.defaults.fontWeight;
  }
}

class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
  static defaultProps = defaultProps;

  isComponentMounted = false;

  createEncoder = (encoding?: Partial<WordCloudEncoding>): SimpleEncoder =>
    new SimpleEncoder(encoding ?? {}, {
      color: this.props.theme.colorTextLabel,
      fontFamily: this.props.theme.fontFamily,
      fontSize: 20,
      fontWeight: 'bold',
      text: '',
    });

  constructor(props: FullWordCloudProps) {
    super(props);
    this.state = {
      words: [],
      scaleFactor: 1,
    };
    this.setWords = this.setWords.bind(this);
  }

  componentDidMount() {
    this.isComponentMounted = true;
    this.update();
  }

  componentDidUpdate(prevProps: WordCloudProps) {
    const { data, encoding, width, height, rotation } = this.props;
    if (
      !isEqual(prevProps.data, data) ||
      !isEqual(prevProps.encoding, encoding) ||
      prevProps.width !== width ||
      prevProps.height !== height ||
      prevProps.rotation !== rotation
    ) {
      this.update();
    }
  }

  componentWillUnmount() {
    this.isComponentMounted = false;
  }

  setWords(words: Word[]) {
    if (this.isComponentMounted) {
      this.setState({ words });
    }
  }

  update() {
    const { data, encoding } = this.props;

    const encoder = this.createEncoder(encoding);
    encoder.setDomainFromDataset(data);

    const sortedData = [...data].sort(
      (a, b) => encoder.getFontSize(b) - encoder.getFontSize(a),
    );
    const topResultsCount = Math.max(
      sortedData.length * TOP_RESULTS_PERCENTAGE,
      10,
    );
    const topResults = sortedData.slice(0, topResultsCount);

    this.generateCloud(encoder, 1, (words: Word[]) =>
      topResults.every((d: PlainObject) =>
        words.find(({ text }) => encoder.getText(d) === text),
      ),
    );
  }

  generateCloud(
    encoder: SimpleEncoder,
    scaleFactor: number,
    isValid: (word: Word[]) => boolean,
  ) {
    const { data, width, height, rotation } = this.props;

    cloudLayout()
      .size([width * scaleFactor, height * scaleFactor])
      .words(data.map((d: Word) => ({ ...d })))
      .padding(5)
      .rotate(ROTATION[rotation] || ROTATION.flat)
      .text((d: PlainObject) => encoder.getText(d))
      .font((d: PlainObject) => encoder.getFontFamily(d))
      .fontWeight((d: PlainObject) => encoder.getFontWeight(d))
      .fontSize((d: PlainObject) => encoder.getFontSize(d))
      .on('end', (words: Word[]) => {
        if (isValid(words) || scaleFactor > MAX_SCALE_FACTOR) {
          this.setWords(words);
        } else {
          this.generateCloud(encoder, scaleFactor + SCALE_FACTOR_STEP, isValid);
        }
      })
      .start();
  }

  render() {
    const { scaleFactor, words } = this.state;
    const { width, height, encoding, sliceId, colorScheme } = this.props;

    const encoder = this.createEncoder(encoding);

    const colorFn = CategoricalColorNamespace.getScale(colorScheme);
    const viewBoxWidth = width * scaleFactor;
    const viewBoxHeight = height * scaleFactor;

    return (
      <svg
        width={width}
        height={height}
        viewBox={`-${viewBoxWidth / 2} -${viewBoxHeight / 2} ${viewBoxWidth} ${viewBoxHeight}`}
      >
        <g>
          {words.map(w => (
            <text
              key={w.text}
              fontSize={`${w.size}px`}
              fontWeight={w.weight}
              fontFamily={w.font}
              fill={colorFn(encoder.getColor(w as PlainObject), sliceId)}
              textAnchor="middle"
              transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
            >
              {w.text}
            </text>
          ))}
        </g>
      </svg>
    );
  }
}

export default withTheme(WordCloud);
