import React from "react";
import moment from "moment";

import eventHandler from "../functions/event-handler";
import deepMerge from "../functions/deep-merge";
import Component from "../component";
import fields from "../forms/fields";
import Icons from "../media/icons";
import JsonRender from "../json-render";
import DropdownContainer from "../containers/dropdown-container";
import Action from "../actions/action";
import i18n from 'i18next';
import global_es from '../../translations/es/global.json'
import global_en from '../../translations/en/global.json'

/**
 * @typedef {Object} FormatOptions
 * @property {string} [format] - Formato para fechas y horas.
 * @property {boolean} [locale] - Si se debe considerar la zona horaria local.
 * @property {string} [currency] - Código de moneda para el formato de moneda.
 * @property {string} ['true'] - Representación en cadena de un valor booleano `true`.
 * @property {string} ['false'] - Representación en cadena de un valor booleano `false`.
 */

/**
 * Objeto con funciones de formateo para diferentes tipos de datos.
 * 
 * @namespace
 * @property {function(any, Object, Object, Object, string): React.Component} component - Formatea los datos a un componente.
 * @property {function(any, FormatOptions=): string} date - Formatea los datos a una fecha.
 * @property {function(any, FormatOptions=): string} datetime - Formatea los datos a un datetime.
 * @property {function(any, FormatOptions=): string} currency - Formatea los datos a una moneda.
 * @property {function(any, FormatOptions=): string} number - Formatea los datos a un número.
 * @property {function(any, Object): string} boolean - Formatea los datos a un booleano.
 */
export const FORMATS = {
  /**
   * Formatea los datos en crudo a un componente.
   *
   * @function
   * @param {any} raw - Los datos en crudo que deben ser formateados.
   * @param {Object} props - Propiedades del componente.
   * @param {Object} data - Datos de la celda.
   * @param {Object} jsonRender - Instancia de JsonRender.
   * @param {String} colName - Nombre de la columna.
   * @returns {React.Component} El componente formateado.
   */
  component: (raw, props, data, jsonRender, colName) => {
    if (props.type === 'boolean') {
      props.value = !!raw;
    } else {
      props.value = raw;
    }
    props.name += '.cell';
    props.id = data.id;
    props.data = data;
    props.columnName = colName;
    return jsonRender.buildContent(props);
  },
  /**
   * Formatea los datos en crudo a una fecha.
   *
   * @function
   * @param {any} raw - Los datos en crudo que deben ser formateados.
   * @param {Object} options - Opciones de formato de la fecha.
   * @returns {String} La fecha formateada.
   */
  date: (
    raw,
    { format: f = 'DD/MM/YYYY', locale = false } = {}
  ) =>
    raw
      ? locale
        ? moment.utc(raw).format(f) // Parse with UTC and format
        : moment.utc(raw).format(f)
      : '',
  /**
   * Formatea los datos en crudo a un datetime.
   *
   * @function
   * @param {any} raw - Los datos en crudo que deben ser formateados.
   * @param {Object} options - Opciones de formato del datetime.
   * @returns {String} El datetime formateado.
   */
  datetime: (
    raw,
    { format: f = 'DD/MM/YYYY HH:mm', locale = false, options } = {}
  ) =>
    raw
      ? locale
        ? moment.utc(raw, options).format(f) // Parse with UTC and format
        : moment.utc(raw, options).format(f)
      : '',
  /**
   * Formatea los datos en crudo a una moneda.
   *
   * @function
   * @param {any} raw - Los datos en crudo que deben ser formateados.
   * @param {Object} options - Opciones de formato de la moneda.
   * @returns {String} La moneda formateada.
   */
  currency: (raw, { locale = 'en-US', currency = 'USD' } = {}) =>
    ['string', 'number'].includes(typeof raw) ?
      (new Intl.NumberFormat(locale, {
        style: 'currency',
        currency,
        minimumFractionDigits: 2,
        maximumFractionDigits: 6
      })).format(raw) : '',
  /**
   * Formatea los datos en crudo a un número.
   *
   * @function
   * @param {any} raw - Los datos en crudo que deben ser formateados.
   * @param {Object} options - Opciones de formato del número.
   * @returns {String} El número formateado.
   */
  number: (raw, { locale = 'en-US' } = {}) =>
    ['string', 'number'].includes(typeof raw) ?
      (new Number(raw)).toLocaleString(locale, {
        minimumFractionDigits: 2,
        maximumFractionDigits: 6
      }) : '',
  /**
   * Formatea los datos en crudo a un booleano.
   *
   * @function
   * @param {any} raw - Los datos en crudo que deben ser formateados.
   * @param {Object} options - Opciones de formato del booleano.
   * @returns {String} El booleano formateado.
   */
  boolean: (raw, { 'true': True = 'Yes', 'false': False = 'Not' }) => (raw ? True : False)
}

/**
 * Agrega nuevas plantillas de formato al objeto FORMATS.
 *
 * Esta función utiliza `Object.assign` para fusionar las plantillas proporcionadas en el objeto FORMATS existente.
 * Esto significa que si una plantilla con el mismo nombre ya existe en FORMATS, la nueva plantilla la sobrescribirá.
 *
 * @function
 * @param {Object} newTemplates - Un objeto que contiene plantillas de formato a agregar. Las claves son los nombres de las plantillas y los valores son las funciones de formato.
 * @returns {void}
 * @example
 * 
 * addFormatTemplates({
 *   newFormat: (raw, options) => { ... },
 *   anotherFormat: (raw, options) => { ... }
 * });
 */
export const addFormatTemplates = (newTemplates = {}) => {
  Object.assign(FORMATS, newTemplates);
}

/**
 * Componente de celda de encabezado para una tabla.
 *
 * @class HeaderCell
 * @extends {React.Component}
 */
export class HeaderCell extends React.Component {

  static jsClass = 'HeaderColumn';
  static defaultProps = {
    filterPos: 'down'
  }

  state = {};

  constructor(props) {
    super(props);
    this.events = [];
  }

  /**
   * Método de ciclo de vida de React que se llama cuando el componente se ha montado.
   * Se suscribe a eventos relevantes para este componente.
   */
  componentDidMount() {
    const { col } = this.props;
    if (col.filter) {
      this.events.push([col.filter.name, this.onChangeFilter]);
    }
    for (const event of this.events) {
      eventHandler.subscribe(...event);
    }
  }

  /**
   * Método de ciclo de vida de React que se llama cuando el componente se actualiza.
   * Restablece la dirección de la clasificación si la clasificación se ha eliminado.
   * @param {Object} prevProps - Las propiedades anteriores del componente.
   * @param {Object} prevState - El estado anterior del componente.
   */
  componentDidUpdate(prevProps, prevState) {
    if (!this.props.sort && this.state.sortDir) this.setState({ sortDir: null });
  }

  /**
   * Método de ciclo de vida de React que se llama cuando el componente está a punto de desmontarse.
   * Cancela la suscripción a todos los eventos a los que se suscribió en componentDidMount.
   */
  componentWillUnmount() {
    for (const event of this.events) {
      eventHandler.unsubscribe(event[0]);
    }
  }

  /**
   * Manejador de eventos para cambios en el filtro.
   * @param {Object} data - Los datos del evento.
   */
  onChangeFilter = (data) => {
    const { filter } = this.props.col;
    const searchActive = !!(Array.isArray(data[filter.name]) ?
      data[filter.name].length : data[filter.name]);
    this.setState({ searchActive });
    eventHandler.dispatch(this.props.tableName, data);
  }

  /**
   * Realiza la acción de ordenar las celdas en el encabezado.
   * @param {string} dir - La dirección en la que se va a realizar la ordenación.
   */
  sort(dir) {
    const { onSort, col } = this.props;
    const { sortDir } = this.state;
    let newDir = dir;
    if (sortDir === newDir) newDir = null;
    this.setState({ sortDir: newDir });
    if (typeof onSort === 'function') onSort(newDir ? { [col.name]: newDir } : null);
  }

  /**
   * Renderiza el componente.
   * @returns {React.Component} El componente renderizado.
   */
  render() {
    const { col, classes, icons, orderable, filterPos, headerClasses, history } = this.props;
    const { sortDir, searchActive } = this.state;
    const showOrder = typeof col.orderable !== 'undefined' ? col.orderable : orderable;
    const translatedLabel = history.type === 'proveedor' ? i18n.t(`${this.props.tableName}.${col.label?.props?.children || ''}`, { ns: 'global' }) : col.label;
    const style = {
      minWidth: col.width
    }
    const cn = [
      'header position-relative w-100',
      col.type, col.name + '-header',
      col.classes, classes
    ];
    const cnSearch = ['cursor-pointer'];
    if (!searchActive) cnSearch.push('text-muted');
    const hClasses = ["align-middle", col.name];
    if (headerClasses) hClasses.push(headerClasses);
    return <th className={hClasses.join(' ')} scope="col">
      <div className="d-flex align-items-center">
        <div className={cn.join(' ')} style={style}>
          <span>{translatedLabel}</span>
        </div>
        <div className="d-flex">
          {col.filter && <div className={"ps-2 mt-1 drop" + filterPos}>
            <DropdownContainer
              name={col.name + 'DropdownFilter'}
              label={<Icons icon={icons.search} className={cnSearch.join(' ')} />}
              dropdownClasses="dropdown-menu-end p-0"
              dropdownClass={false}
            >
              {searchActive && <Action
                name={col.name + 'Clear'}
                classes="btn-link btn-sm p-0"
                style={{ top: 5, position: 'absolute', right: 8, zIndex: 4 }}
              >
                <Icons icon={icons.clear} classes="text-danger" />
              </Action>}
              {React.createElement(fields[col.filter.type] || fields.Field, col.filter)}
            </DropdownContainer>
          </div>}
          {showOrder && <div className="ps-2 text-muted" style={{ fontSize: 10 }}>
            <span onClick={(e) => this.sort('DESC', e)}>
              <Icons icon={icons.caretUp}
                className={'cursor-pointer ' + (sortDir === 'DESC' ? 'text-body' : '')}
              />
            </span>
            <span onClick={(e) => this.sort('ASC', e)}>
              <Icons icon={icons.caretDown}
                className={'cursor-pointer ' + (sortDir === 'ASC' ? 'text-body' : '')}
              />
            </span>
          </div>}
        </div>
      </div>
    </th>
  }

}

/**
* Clase base para la tabla.
*
* Este componente es responsable de renderizar una tabla a partir de un conjunto de datos proporcionado.
* También proporciona funcionalidad para ordenar, filtrar y manejar eventos.
*
* @class Table
* @extends {Component}
*/
export default class Table extends Component {

  static jsClass = 'Table';
  static defaultProps = {
    ...Component.defaultProps,
    data: [],
    striped: true,
    hover: true,
    icons: {
      caretUp: 'caret-up',
      caretDown: 'caret-down',
      search: 'search',
      clear: 'close'
    },
    vertical: false
  }

  state = {};

  constructor(props) {
    super(props);
    this.jsonRender = new JsonRender(props, props.mutations);
    this.t = i18n.t.bind(i18n);
  }

  /**
   * Subscribes to the events when the component is mounted.
   *
   * @method componentDidMount
   * @memberof Table
   */
  componentDidMount() {
    this.events = [];
    Object.entries(this.props.columns).forEach(([key, col]) => {
      // TODO: mejorar esto, filtrar el componente dropdown
      // buscar una forma de que solo los componentes field carguen evento
      if (col.format === 'component' && col.formatOpts.component !== 'DropdownButtonContainer') {
        const event = [col.formatOpts.name + '.cell', this.onEventCell];
        this.events.push(event);
        eventHandler.subscribe(...event);
      }
    });
    // Initialize i18n
    i18n.init({
      lng: this.props.history.lang,  // Set default language
      resources: {
        en: {
          global: global_en
        },
        es: {
          global: global_es
        }
      }
    })
  }

  /**
   * Unsubscribes from the events when the component is unmounted.
   *
   * @method componentWillUnmount
   * @memberof Table
   */
  componentWillUnmount() {
    for (const [eventName] of this.events) {
      eventHandler.unsubscribe(eventName);
    }
  }

  // Events
  /**
   * Handles sorting event.
   *
   * @method onSort
   * @param {Object} orderBy - An object representing the column to be sorted.
   * @memberof Table
   */
  onSort = (orderBy) => {
    const { onChange } = this.props;
    this.setState({ orderBy: orderBy && Object.keys(orderBy).pop() });
    if (typeof onChange === 'function') onChange({ orderBy });
  }

  /**
   * Handles cell events.
   *
   * @method onEventCell
   * @param {Object} dataRaw - Raw data of the event.
   * @memberof Table
   */
  onEventCell = (dataRaw) => {
    const data = {};
    for (const event in dataRaw) {
      data[event.replace(/\.cell$/, '')] = dataRaw[event];
    }
    eventHandler.dispatch(this.props.name, data);
  }
  //------

  /**
   * Maps HeaderCell components for each column.
   *
   * @method mapHeaderCell
   * @param {Array} args - The column properties.
   * @param {number} i - The index of the column.
   * @returns {React.Component} - A HeaderCell component.
   * @memberof Table
   */
  mapHeaderCell = ([key, col], i) => {
    const { colClasses, headerClasses, icons, orderable, name, history} = this.props;
    const { orderBy } = this.state;
    col.name = col.name || key;
    col.label = this.jsonRender.buildContent(col.label);
    const props = {
      col,
      orderable,
      classes: colClasses,
      headerClasses,
      icons,
      onSort: this.onSort,
      sort: orderBy === col.name,
      tableName: name,
      history
    };

    return <HeaderCell key={i + '-' + col.name} {...props} />
  }

  /**
   * Provides properties for a row.
   *
   * @method rowProps
   * @param {Object} rowOrColumn - The data of the row or column.
   * @param {number} i - The index of the row or column.
   * @returns {Object} - Properties for the row.
   * @memberof Table
   */
  rowProps = (rowOrColumn, i) => {
    const { name, mapRows: mapRowsFunc, vertical } = this.props;
    const id = vertical ? rowOrColumn.name : rowOrColumn.id;
    const rowKey = (!!id || id === 0) ? (i + '-' + id) : i;
    const cnRow = ['row-' + this.props.name, 'row-' + ((!!id || id === 0) ? id : i)];
    const { classes: rowClasses, ...rowProps } =
      (typeof mapRowsFunc === 'function' && mapRowsFunc(name, rowOrColumn, i)) || {};
    if (rowClasses) cnRow.push(rowClasses);

    return {
      key: rowKey,
      ...rowProps,
      className: cnRow.join(' ')
    };
  };

  /**
   * Maps cell components for each cell in a row.
   *
   * @method mapCell
   * @param {Object} rowData - The data of the row.
   * @param {Object} col - The properties of the column.
   * @param {number} i - The index of the cell.
   * @returns {React.Component} - A cell component.
   * @memberof Table
   */
  mapCell = (rowData, col, i) => {
    const { mapCells: mapCellsFunc, name, colClasses, vertical } = this.props;
    const colName = col.name;
    const cellAttrs = {
      className: ['cell', col.type, col.name + '-cell', col.classes, colClasses],
      style: vertical
        ? {
          minWidth: col.width,
          ...col.style
        }
        : col.style,
      title: typeof rowData[colName] !== 'object' ? rowData[colName] : undefined
    }

    const mutation = typeof mapCellsFunc === 'function' && mapCellsFunc(name, col.name, rowData, cellAttrs) || {};
    let formatOptions;
    if (col.formatOpts) {
      formatOptions = deepMerge({ ...col.formatOpts }, mutation);
    } else {
      if (mutation.classes) {
        const classes = mutation.classes;
        delete mutation.classes;
        cellAttrs.className.push(classes);
      }
      deepMerge(cellAttrs, mutation);
    }
    cellAttrs.className = cellAttrs.className.flat().join(' ');

    const formater = FORMATS[col.format] || (raw => raw);
    const cellData = typeof rowData[col.name] !== 'undefined' ? rowData[col.name] : true;

    const cell = (<div {...cellAttrs}> {formater(cellData, formatOptions, rowData, this.jsonRender, colName)}  </div>);
    return (colName === 'id' ?
      <th key={i + '-' + colName} className={colName} scope="row">{cell}</th> :
      <td key={i + '-' + colName} className={vertical ? [colName, 'row-' + (i - 1)].join(' ') : colName} >{cell}</td>
    );
  }

  /**
   * Renders the table content.
   *
   * @method content
   * @param {Array} children - Optional children to be rendered in the table.
   * @returns {React.Component} - The rendered table.
   * @memberof Table
   */
  content(children = this.props.children) {
    const { data, columns, tableClasses, hover, striped, vertical } = this.props;

    // Definir encabezado y pie de página si están presentes
    let header, footer;
    if (Array.isArray(children))
      ([header, footer] = children);

    // Construcción de las clases CSS para la tabla
    const cn = ['table'];
    if (striped) cn.push('table-striped');
    if (hover) cn.push('table-hover');
    if (tableClasses) cn.push(tableClasses);
    if (vertical) cn.push('vertical');

    // Inicialización de los datos de la tabla según la disposición (vertical/horizontal)
    const tableData = [];

    // Procesamiento de los datos para construir la estructura de la tabla
    data.forEach((row, i) => {
      Object.entries(columns).forEach(([key, col], j) => {
        const column = col.name ? col : { name: key, ...col };

        if (!vertical) {
          // Si la tabla no es vertical (es decir, es horizontal)

          // Si no se ha inicializado el registro de la tabla (fila) en la posición i, se inicializa
          if (!tableData[i]) tableData[i] = { cells: [], data: row };

          // Se mapea la celda en la posición j de la fila i, pasándole los datos de la fila y la columna
          tableData[i].cells[j] = this.mapCell(row, column, j);
        } else {
          // Si la tabla es vertical

          // Si no se ha inicializado el registro de la tabla (columna) en la posición j, se inicializa
          if (!tableData[j]) tableData[j] = { cells: [], data: column };

          // Para la primera fila de cada columna (i === 0), se mapea la celda de encabezado
          if (i === 0) tableData[j].cells[0] = this.mapHeaderCell([key, column], 0);

          // Para las filas restantes de cada columna, se mapea la celda correspondiente
          const k = i + 1;
          tableData[j].cells[k] = this.mapCell(row, column, k);
        }

      });
    });

    // Renderización de la tabla con las estructuras de encabezado, cuerpo y pie de página
    return (
      <table className={cn.join(' ')}>
        <thead>
          {header && (
            <tr>
              <td colSpan="1000">
                <div>{header}</div>
              </td>
            </tr>
          )}
          {!vertical && (
            <tr >
              {Object.entries(columns).map(this.mapHeaderCell)}
            </tr>
          )}
        </thead>
        <tbody>
          {tableData.map(({ cells, data }, i) => <tr {...this.rowProps(data, i)}>{cells}</tr>)}
        </tbody>
        {footer && (
          <tfoot>
            <tr>
              <td colSpan="1000">
                <div>{footer}</div>
              </td>
            </tr>
          </tfoot>
        )}
      </table>
    );
  }

}