import { values } from '../utils/obj/values';
import { uuidv4 } from '@firebase/util';
import { makeAutoObservable } from 'mobx';
import {
  rbgaStringToHex,
  toApphouseColors,
  toColorsObjectFlat
} from './utils/color.utils';
import { toApphouseStyles } from './utils/styles.utils';
import {
  getColorTokensLookupReferenceFromPalettes,
  getLookupTable,
  getTokenListId,
  hasColorReference,
  toApphouseTokens,
  toTokensObject
} from './utils/tokens.utils';
import { Color, getDefaultColors } from './Color';
import { Palette } from './Palette';
import { Style } from './Style';
import { getValueFromReferenceString } from './utils/theme.utils';
import { Token } from './Token';
import { ApphouseTheme, ThemeFull } from '../styles/defaults/themes.interface';
import { BaseThemeSettings } from './defaults.tokens';
import { ApphousePaletteModeOptions } from '../constants/constants';
import { ThemeUpdatesTracker } from './ThemeUpdatesTracker';
import { objThemeToTheme, PaletteUtils } from './utils/theme.conversion.utils';
import { ApphouseThemeTokens } from '../styles/defaults/app.token.values';
import { ColorDefinition } from './utils/color.interface';
import {
  BaseComponentTypeOption,
  CssPropertyStyle,
  StyleType,
  TagPreviewOption
} from './style.interface';
import {
  TOKEN_KEY_SEPARATOR,
  TokenType,
  TokenTypeOption,
  tokenTypes
} from './token.interface';
import { PaletteType, ThemeModeType } from './palette.interface';

type LookupTable = { [id: string]: string };

const presentationModeOptions = [
  'styles',
  'tokens',
  'colors',
  'palette',
  'all',
  ...tokenTypes
];

export type PresentationModeOption =
  | (typeof presentationModeOptions)[number]
  | string;

export class Theme {
  title: string;
  id: string;
  palette: Record<string, Palette>;
  tokens: Record<string, Token>;
  styles: Record<string, Style>;
  sync: ThemeUpdatesTracker;
  presentationMode?: PresentationModeOption;

  constructor(prop: ThemeFull) {
    this.title = prop.title || 'Untitled Design System';
    this.id = prop.id || uuidv4();
    const theme = objThemeToTheme(prop);
    this.palette = theme.palette || {};
    this.tokens = theme.tokens || {};
    this.styles = theme.styles || {};
    this.sync = new ThemeUpdatesTracker();

    makeAutoObservable(this);
  }

  get paletteColorsIds(): string[] {
    return Object.keys(this.palette).map((key) => {
      return this.palette[key].id;
    });
  }

  /**
   * If the theme has any palette with mode === "theme", the theme will have
   * themed styles. hasThemedStyles is helpful for determining how to extract
   * the values from the styles when importing / exporting. If the theme
   * has themed styles, the user will probably have to select a palette id
   * to extract the raw values from.
   */
  get hasThemedStyles(): boolean {
    // A theme has themed styles if it has a palette with a mode of "theme"
    const themedColors = Object.keys(this.palette).filter((key) => {
      return this.palette[key].mode === 'theme';
    });
    if (themedColors.length > 0) {
      return true;
    }
    return false;
  }

  get lookup(): LookupTable {
    const colorsLookup = getLookupTableForCurrentThemePalettes(this.palette);
    const tokensLookup = this.tokensLookupTable;

    return { ...colorsLookup, ...tokensLookup };
  }

  get hasPaletteDefined(): boolean {
    if (Object.keys(this.palette).length > 0) {
      return true;
    }
    return false;
  }

  get hasTokensDefined(): boolean {
    if (Object.keys(this.tokens).length > 0) {
      return true;
    }
    return false;
  }

  get hasStylesDefined(): boolean {
    if (Object.keys(this.styles).length > 0) {
      return true;
    }
    return false;
  }

  get stylesGroupedByBaseComponentType(): { [baseComponent: string]: Style[] } {
    const grouped: { [baseComponent: string]: Style[] } = {};
    values(this.styles).forEach((style) => {
      const baseComponentType = style.baseComponent;
      if (!grouped[baseComponentType]) {
        grouped[baseComponentType] = [];
      }
      grouped[baseComponentType].push(style);
    });
    return grouped;
  }

  get tokensByType(): Record<string, Token[]> {
    const tokens: Record<string, Token[]> = {};
    Object.keys(this.tokens).forEach((key) => {
      const type = key.split(TOKEN_KEY_SEPARATOR);
      if (tokens[type[0]]) {
        tokens[type[0]] = [...tokens[type[0]], this.tokens[key]];
      } else {
        tokens[type[0]] = [this.tokens[key]];
      }
    });
    return tokens;
  }

  get basePalette() {
    return values(this.palette).find((p) => p.mode === 'base');
  }

  get tokensLookupTable(): LookupTable {
    const lookup = getLookupTable(toTokensObject(this.tokens));
    return { ...lookup };
  }

  get reverseTokensLookupTable(): { [id: string]: string } {
    const lookup: { [id: string]: string } = {};
    Object.keys(this.tokensLookupTable).forEach((key) => {
      const value = this.tokensLookupTable[key];
      if (hasColorReference(key)) {
        // it is a color token
        // TODO: do something about that, maybe?
        lookup[value] = key;
      } else {
        lookup[value] = key;
      }
    });
    const colors: any = toColorsObjectFlat(this.palette);
    const colorsLookup: { [id: string]: string } = {};

    colors.base &&
      Object.keys(colors.base)?.forEach((paletteID) => {
        const c: any = colors.base[paletteID];

        c &&
          Object.keys(c)?.forEach((colorID) => {
            const rgba = c[colorID].rgba;
            colorsLookup[`base.${colorID}`] = rgba;
          });
      });

    colors.theme &&
      Object.keys(colors.theme)?.forEach((paletteID) => {
        const c: any = colors.theme[paletteID];

        c &&
          Object.keys(c)?.forEach((colorID) => {
            const rgba = c[colorID].rgba;
            colorsLookup[`theme.${paletteID}.${colorID}`] = rgba;
          });
      });
    return { ...lookup, ...colorsLookup };
  }

  static autoGenId = (title: string) => {
    if (!title) {
      return uuidv4();
    }
    return title.split(' ').join('-').toLocaleLowerCase();
  };

  get colors(): Color[] {
    let colors: Color[] = [];
    Object.keys(this.palette).forEach((paletteId) => {
      const paletteColors = this.palette[paletteId].colors;
      Object.keys(paletteColors).forEach((id) => {
        const color = paletteColors[id];
        colors = [...colors, color];
      });
    });

    return colors;
  }

  get colorsByPalette(): Record<string, Color[]> {
    const colors: Record<string, Color[]> = {};
    Object.keys(this.palette).forEach((paletteId) => {
      colors[paletteId] = [];
      const paletteColors = this.palette[paletteId].colors;
      Object.keys(paletteColors).forEach((id) => {
        const color = paletteColors[id];
        colors[paletteId] = [...colors[paletteId], color];
      });
    });

    return colors;
  }

  get colorsByMode(): Record<string, Color[]> {
    const colors: Record<string, Color[]> = {};
    Object.keys(this.palette).forEach((paletteId) => {
      const palette = this.palette[paletteId];
      const mode = palette.mode;
      let prefix: string = mode;
      if (mode === 'base') {
        prefix = `base.${paletteId}`;
      } else {
        prefix = `theme.${mode}`;
      }
      colors[prefix] = [];
      const paletteColors = palette.colors;
      Object.keys(paletteColors).forEach((id) => {
        const color = paletteColors[id];
        colors[prefix] = [...colors[prefix], color];
      });
    });

    return colors;
  }

  get modeColorsLength(): number {
    let len = 0;
    values(this.colorsByMode).forEach((colors) => {
      len = len + colors.length;
    });
    return len;
  }

  get swatchColors(): ColorDefinition[] {
    let colorDefinitions: ColorDefinition[] = [];
    Object.keys(this.palette).forEach((paletteId) => {
      const paletteColors = this.palette[paletteId].colors;
      Object.keys(paletteColors).forEach((id) => {
        const color = paletteColors[id];
        colorDefinitions = [...colorDefinitions, color.color];
      });
    });

    return colorDefinitions;
  }

  // cloning the theme means changing the theme id to a brand new id
  setNewUniqueId = () => {
    this.id = uuidv4();
    return this.id;
  };

  /**
   * Clone this theme
   * @returns a new theme with a copy of this one, but with a new id
   */
  clone = () => {
    const newTheme = this;
    newTheme.setNewUniqueId();
    return newTheme;
  };

  getThemeObject = (
    withColorsFromPaletteId: string
  ): ApphouseTheme | undefined => {
    const currentStyles = this.styles;
    const currentTokens = this.tokens;
    const currentPalette = this.getPalette('theme', withColorsFromPaletteId);

    if (currentPalette) {
      const colors = toApphouseColors(currentPalette);
      const tokens = toApphouseTokens(currentTokens);
      // Lookup should have all the tokens and colors in raw form
      const lookupTokens = getLookupTable(tokens);
      const lookupColors = getLookupTable(colors, {}, 'theme');
      const lookup = { ...lookupTokens, ...lookupColors };

      const Apphouse: ApphouseTheme = {
        colors,
        tokens,
        styles: toApphouseStyles({
          value: currentStyles || {},
          withColorsFromPaletteId: withColorsFromPaletteId,
          lookup
        })
      };

      return Apphouse;
    }

    return undefined;
  };

  getColorsFromPalettesFromColorId = (
    colorId?: string
  ): { palette: string; rgba: string; color: Color }[] => {
    let colors: { palette: string; rgba: string; color: Color }[] = [];
    if (colorId) {
      Object.keys(this.palette).forEach((paletteId) => {
        const paletteColors = this.palette[paletteId].colors;
        const c = paletteColors[colorId];
        colors = [
          ...colors,
          {
            palette: this.palette[paletteId].title,
            rgba: c?.rgbString || 'Unknown color',
            color: paletteColors[colorId]
          }
        ];
      });
    }
    return colors;
  };

  setPresentationMode = (value: PresentationModeOption | undefined) => {
    this.presentationMode = value;
  };

  setTitle = (value: string) => {
    this.title = value;
  };

  setId = (value: string) => {
    this.id = value;
  };

  styleStyles = (styles: Record<string, Style>) => {
    this.styles = styles;
  };
  setStyle = (style: Style) => {
    this.styles[style.id] = style;
  };

  addTokenList = (tokens: Token[]) => {
    tokens.forEach((token) => {
      this.setToken(token);
    });
  };

  addStyles = (styles: StyleType[]) => {
    if (Array.isArray(styles)) {
      styles.forEach((style) => {
        this.styles[style.id] = new Style(style);
      });
    } else {
      // WARNING: Getting here is not expected behavior. We are expecting an array of styles
      // but we are getting an object of styles. This can happen if the user is
      // importing a theme that has been exported from the apphouse theme editor or
      // it does not have the correct format. We attempt to convert the the correct format
      // but it might not work.
      if (typeof styles === 'object') {
        console.log('TODO: convert styles to array');
      }
    }
  };

  setToken = (token: Token) => {
    this.tokens[getTokenListId(token)] = token;
  };

  setTokens = (tokens: Record<string, TokenType>) => {
    Object.keys(tokens).forEach((key) => {
      const token = tokens[key];
      this.tokens[getTokenListId(token)] = new Token(token);
    });
  };

  importPalette = (objPalette: PaletteType) => {
    const palette = PaletteUtils.objPaletteToPalette(objPalette);
    this.palette[palette.id] = palette;
  };

  importPalettes = (objPalettes: PaletteType[]) => {
    objPalettes.forEach((objPalette) => {
      this.importPalette(objPalette);
    });
  };

  resetStyles = () => {
    this.styles = {};
  };

  resetTokens = () => {
    this.tokens = {};
  };

  setTokensTokens = (tokens: Record<string, Token>) => {
    this.tokens = tokens;
  };

  addToken = (key: string, value: string, type: TokenTypeOption) => {
    const id = `${type}${TOKEN_KEY_SEPARATOR}${key}`.trim();
    if (this.tokens[id]) {
      // update token instead
      this.tokens[id].setKey(key);
      this.tokens[id].setValue(value);
      this.tokens[id].setType(type);
    } else {
      const token = new Token({ key, value, type });
      this.tokens[id] = token;
    }
  };

  addPalette = (key: string, value: PaletteType) => {
    this.palette[key] = new Palette(value);
  };

  addPalettes = (palettes: { [paletteId: string]: Palette }) => {
    this.palette = palettes;
  };

  setPalette = (palette: Palette) => {
    this.palette[palette.id] = palette;
  };

  addStyle = (
    id: string,
    value: CssPropertyStyle[],
    baseComponent: BaseComponentTypeOption,
    variant: string,
    state: string,
    previewWithTag: TagPreviewOption
  ) => {
    const style = new Style({
      id,
      value,
      baseComponent,
      variant,
      state,
      previewWithTag
    });
    this.styles[id] = style;
  };

  appendStyles = (styles: { [selector: string]: Style }) => {
    styles &&
      Object.keys(styles).forEach((key) => {
        this.styles[key] = styles[key];
      });
  };

  removePalette = (key: string) => {
    if (this.palette[key]) {
      delete this.palette[key];
    }
  };

  removePaletteColor = (paletteId: string, colorId: string): boolean => {
    if (this.palette[paletteId]) {
      if (this.palette[paletteId].colors[colorId]) {
        delete this.palette[paletteId].colors[colorId];
        return true;
      }
    }
    return false;
  };

  removeToken = (key: string) => {
    if (this.tokens[key]) {
      delete this.tokens[key];
    }
  };
  removeStyle = (key: string): boolean => {
    if (this.styles[key]) {
      delete this.styles[key];
      return true;
    }
    return false;
  };

  deleteAllStylesWithKey = (
    keyInStyle:
      | 'id'
      | 'variant'
      | 'baseComponent'
      | 'value'
      | 'state'
      | 'previewWithTag',
    value: string
  ) => {
    Object.keys(this.styles).forEach((key) => {
      const style = this.styles[key];
      if (style[keyInStyle] === value) {
        delete this.styles[key];
      }
    });
  };

  deleteAllTokenTypes = (type: string) => {
    Object.keys(this.tokens).forEach((id) => {
      const token = this.tokens[id];

      if (token.type === type) {
        delete this.tokens[id];
      }
    });
  };

  getColorKey = (colorDefinition: ColorDefinition): string | undefined => {
    let colorKey: string | undefined = undefined;
    this.colors.forEach((color) => {
      if (
        JSON.stringify(color.color.rgb) === JSON.stringify(colorDefinition.rgb)
      ) {
        colorKey = color.id;
      }
    });

    return colorKey;
  };

  getPalette = (mode: ThemeModeType, id: string): Palette | undefined => {
    let wantedPalette: Palette | undefined = undefined;
    Object.keys(this.palette).forEach((paletteId) => {
      const palette = this.palette[paletteId];
      if (palette.mode === mode && paletteId === id) {
        wantedPalette = palette;
      }
    });
    return wantedPalette;
  };

  getColorFromColorId = (colorId: string): Color | undefined => {
    const result = this.colors?.find(
      (color) => color.id === colorId.toLocaleLowerCase()
    );
    return result;
  };

  getFlattenTokensByType = (flattenByType?: string): Record<string, any> => {
    const flatten: any = {};

    Object.keys(this.palette).forEach((paletteId) => {
      flatten[paletteId] = {};
      Object.keys(this.tokensByType).forEach((type) => {
        if (flattenByType && flattenByType !== type) {
          return;
        }
        flatten[paletteId][type] = {};
        const tokens: Token[] = this.tokensByType[type];
        tokens.forEach((token) => {
          //TODO

          let value;

          value = getValueFromReferenceString({
            referenceString: token.value,
            colors: this.palette,
            tokens: this.tokens,
            theme: paletteId as any,
            withRaw: true
          });

          // let's prefer hex value over rgb
          if (typeof value === 'string' && value?.startsWith('rgba')) {
            value = rbgaStringToHex(value);
          }
          flatten[paletteId][type][token.key] = value;
        });
      });
    });
    return flatten;
  };

  static toCamelCase = (str: string) => {
    const tocamelcase = (word: string) => {
      return `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`;
    };
    const camelcasefy = (dashedProperty: string) => {
      const dashedPropertySplit = dashedProperty.split('-');
      if (dashedPropertySplit.length <= 1) return dashedProperty;
      const camelcase: string[] = [];
      dashedPropertySplit.forEach((word, i) => {
        if (i === 0) {
          camelcase.push(word);
        } else {
          camelcase.push(tocamelcase(word));
        }
      });
      return camelcase.join('');
    };

    return camelcasefy(str);
  };

  reset = () => {
    this.palette = {};
    this.styles = {};
    this.tokens = {};
  };
}

export const generateDefaultThemeFromThemeSettings = (
  themeSettings: BaseThemeSettings
): Theme => {
  const styles = Object.keys(themeSettings.styles).map(
    (key) => themeSettings.styles[key]
  );
  console.log({ styles });
  const defaultDarkId = 'dark';
  const defaultLightId = 'light';
  const dark: PaletteType = {
    title: 'Dark',
    id: defaultDarkId,
    description: 'Dark theme colors',
    mode: 'theme',
    colors: getDefaultColors(ApphousePaletteModeOptions.dark)
  };
  const light: PaletteType = {
    title: 'Light',
    id: defaultLightId,
    description: 'Light theme colors',
    mode: 'theme',
    colors: getDefaultColors(ApphousePaletteModeOptions.light)
  };
  const base: PaletteType = {
    title: 'Base Colors',
    id: 'base',
    description: 'All colors available for this theme',
    mode: 'base',
    colors: getDefaultColors(ApphousePaletteModeOptions.base)
  };
  return new Theme({
    title: 'Sample Design System',
    id: uuidv4(),
    colors: [base, dark, light],
    tokens: Object.keys(themeSettings.tokens).map(
      (key) => themeSettings.tokens[key]
    ),
    styles
  });
};

export const getLookupTableForThisAppColors = () => {
  const palettes: Record<string, Palette> = {};
  palettes['dark'] = new Palette({
    title: 'Dark',
    id: 'dark',
    description: 'Dark theme colors',
    mode: 'theme',
    colors: getDefaultColors(ApphousePaletteModeOptions.dark)
  });
  palettes['light'] = new Palette({
    title: 'Light',
    id: 'light',
    description: 'Light theme colors',
    mode: 'theme',
    colors: getDefaultColors(ApphousePaletteModeOptions.light)
  });
  palettes['base'] = new Palette({
    title: 'Base Colors',
    id: 'base',
    description: 'All colors available for this theme',
    mode: 'base',
    colors: getDefaultColors(ApphousePaletteModeOptions.base)
  });

  return {
    ...getColorTokensLookupReferenceFromPalettes(palettes, 'base'),
    ...getColorTokensLookupReferenceFromPalettes(palettes, 'base', 'dark'),
    ...getColorTokensLookupReferenceFromPalettes(palettes, 'base', 'light')
  };
};

export const getLookupTableForThisApp = () => {
  const colorsLookup = getLookupTableForThisAppColors();
  const tokensLookup = getLookupTable(ApphouseThemeTokens, {});

  return { ...colorsLookup, ...tokensLookup };
};

export const getLookupTableForCurrentThemePalettes = (
  palettes: Record<string, Palette>
) => {
  let lookup = {};
  let basePaletteId = '';
  Object.keys(palettes).forEach((paletteId) => {
    const paletteMode = palettes[paletteId].mode;
    if (paletteMode === 'base') {
      basePaletteId = palettes[paletteId].id;
      lookup = {
        ...lookup,
        ...getColorTokensLookupReferenceFromPalettes(palettes, 'base')
      };
    }
  });

  Object.keys(palettes).forEach((paletteId) => {
    const paletteMode = palettes[paletteId].mode;
    if (paletteMode === 'theme') {
      lookup = {
        ...lookup,
        ...getColorTokensLookupReferenceFromPalettes(
          palettes,
          basePaletteId,
          paletteId
        )
      };
    }
  });

  return lookup;
};
