import { Component, ScopeBase } from "@ribajs/core";
import {
  ShopifyProductVariant,
  ShopifyProduct,
  ShopifyProductVariantOption,
  ShopifyCartService,
  ShopifyProductService,
} from "@ribajs/shopify";
import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";

export interface PreparedProductVariant extends ShopifyProductVariant {
  images?: string[];
}

export interface Scope extends ScopeBase {
  handle: string | null;
  product: ShopifyProduct | null;
  variant: PreparedProductVariant | null;
  quantity: number;
  showDetailMenu: boolean;
  // showAddToCartButton: boolean;
  chooseOption: ShopifyProductComponent["chooseOption"];
  addToCart: ShopifyProductComponent["addToCart"];
  toggleDetailMenu: ShopifyProductComponent["toggleDetailMenu"];
  decrease: ShopifyProductComponent["decrease"];
  increase: ShopifyProductComponent["increase"];
  /**
   * If the variant is available, used to disable the add to cart button
   */
  available: boolean;
}

export class ShopifyProductComponent extends Component {
  public static tagName = "shopify-product";

  protected autobind = true;

  /**
   * handle is the product handle to get the product json object
   * extras are product data which is only available over liquid and not over the product json object
   */
  static get observedAttributes(): string[] {
    return ["handle", "extras"];
  }

  public scope: Scope = {
    handle: null,
    product: null,
    variant: null,
    quantity: 1,
    showDetailMenu: false,
    // showAddToCartButton: false,
    chooseOption: this.chooseOption,
    addToCart: this.addToCart,
    toggleDetailMenu: this.toggleDetailMenu,
    decrease: this.decrease,
    increase: this.increase,
    /**
     * If the variant is available, used to disable the add to cart button
     */
    available: false,
  };

  private colorOption: ShopifyProductVariantOption | null = null;

  private selectedOptions: string[] = [];

  /**
   * Is true if the user has chosen an option
   */
  private optionChosen = false;

  protected set product(product: ShopifyProduct | null) {
    if (product) {
      this.scope.product = ShopifyProductService.prepare(product);
      this.colorOption =
        ShopifyProductService.getOption(this.scope.product, "color") || null;
      this.variant = this.scope.product.variants[0];
    }
  }

  protected get product(): ShopifyProduct | null {
    return this.scope.product;
  }

  protected set variant(variant: ShopifyProductVariant | null) {
    if (variant === null) {
      return;
    }
    this.scope.variant = this.prepareVariant(variant);
    if (this.scope.variant) {
      this.selectedOptions = this.scope.variant.options.slice();
      this.available = this.scope.variant.available;
      this.activateOptions();
    }
  }

  protected get variant() {
    return this.scope.variant;
  }

  /**
   * available is only true if the variant is available and the user has clicked on an option
   */
  protected set available(available: boolean) {
    this.scope.available = available && this.optionChosen;
  }

  constructor() {
    super();
    this.init(ShopifyProductComponent.observedAttributes);
  }

  public chooseOption(
    optionValue: string | number,
    position1: number,
    optionName: string,
    event: MouseEvent,
  ) {
    if (!this.scope.product) {
      throw new Error("Product not set!");
    }

    optionValue = optionValue.toString();

    this.selectedOptions[position1 - 1] = optionValue.toString();
    const variant = ShopifyProductService.getVariantOfOptions(
      this.scope.product,
      this.selectedOptions,
    );
    if (variant) {
      // Option chosen so enable add to cart button
      this.optionChosen = true;

      this.variant = variant as ShopifyProductVariant;
    }

    event.stopPropagation();
  }

  public addToCart() {
    if (!this.variant) {
      return;
    }
    ShopifyCartService.add(this.variant.id, this.scope.quantity)
      .then((response) => {
        console.debug("addToCart response", response);
      })
      .catch((error) => {
        console.error("addToCart error", error);
      });
  }

  public toggleDetailMenu() {
    this.scope.showDetailMenu = !this.scope.showDetailMenu;
  }

  public increase() {
    this.scope.quantity++;
  }

  public decrease() {
    this.scope.quantity--;
    if (this.scope.quantity <= 0) {
      this.scope.quantity = 1;
    }
  }

  /**
   * Workaround because `rv-class-active="isOptionActive | call size"` is not updating if selectedOptions changes
   * @param optionValue
   * @param optionName
   */
  protected activateOption(optionValue: string, optionName: string) {
    optionValue = optionValue.toString().replace("#", "");
    this.querySelector<HTMLElement>(
      `.option-${optionName.toLocaleLowerCase()}`,
    )?.classList.remove("active");
    this.querySelector<HTMLElement>(
      `.option-${optionName.toLocaleLowerCase()}-${optionValue}`,
    )?.classList.add("active");
  }

  /**
   * Activate option by selected options (scope.selectedOptions)
   * This method sets the active class to the options elements
   */
  protected activateOptions() {
    for (const position0 in this.selectedOptions) {
      if (this.selectedOptions[position0]) {
        const optionValue = this.selectedOptions[position0];
        if (this.scope.product) {
          const optionName = this.scope.product.options[position0].name;
          // Only activate size if it was clicked by the user
          if (optionName === "size") {
            if (this.optionChosen) {
              this.activateOption(optionValue, optionName);
            }
          } else {
            this.activateOption(optionValue, optionName);
          }
        }
      }
    }
  }

  protected async beforeBind() {
    await super.beforeBind();
    if (this.scope.handle === null) {
      throw new Error("Product handle not set");
    }
    return ShopifyProductService.get(this.scope.handle).then(
      (product: ShopifyProduct) => {
        this.product = product;
      },
    );
  }

  protected async afterBind() {
    this.activateOptions();
    await super.afterBind();
  }

  protected requiredAttributes(): string[] {
    return ["handle"];
  }

  protected async template() {
    // Only set the component template if there no childs already
    if (this && hasChildNodesTrim(this)) {
      return null;
    } else {
      const { default: template } =
        await import("./product.component.html?raw");
      return template;
    }
  }

  /**
   * custom version of images.indexOf but compares without protocol and query string in url
   * @param images
   * @param findImage
   */
  private indexOfUrl(images: string[], findImage: string) {
    let index = -1;
    const clearFindImage = findImage
      .split("?")[0] // remove query string
      .replace(/(^\w+:|^)\/\//, "//"); // remove protocol
    images.forEach((image, i) => {
      const clearImage = image
        .split("?")[0] // remove query string
        .replace(/(^\w+:|^)\/\//, "//"); // remove protocol
      if (clearImage === clearFindImage) {
        index = i;
      }
    });
    return index;
  }

  /**
   * Get images which are not linked to any variant
   */
  private getGeneralImages(optionName = "color") {
    optionName = optionName.toLowerCase();
    const generalImages: string[] = [];

    if (this.scope.product) {
      // add images without optionName in filename
      this.scope.product.images.forEach((image: string) => {
        if (!image.toLowerCase().includes(`${optionName}-`)) {
          generalImages.push(image);
        }
      });

      // remove variant images from copied array
      this.scope.product.variants.forEach((variant: ShopifyProductVariant) => {
        let index = -1;
        if (variant.featured_image !== null && variant.featured_image.src) {
          index = this.indexOfUrl(generalImages, variant.featured_image.src);
        }
        if (index >= 0) {
          generalImages.splice(index, 1);
        }
      });
    }

    return generalImages;
  }

  /**
   * Get options images (without featured image) filtered by filename.
   * Shopify only supports one image per variant, with this function more images for each variant are possible.
   * The image filename must include {optionName}-{optionValue} for that.
   */
  private getOptionImages(
    option: ShopifyProductVariantOption,
    optionValue: string,
  ) {
    optionValue = optionValue.toLowerCase().replace("#", "_");
    const optionName = option.name.toLowerCase();
    const optionImages: string[] = [];
    if (this.scope.product) {
      this.scope.product.images.forEach((image: string) => {
        if (image.toLowerCase().includes(`${optionName}-${optionValue}`)) {
          optionImages.push(image);
        }
      });
    }

    return optionImages;
  }

  /**
   * Get featured images of variant, use the first option image or the featured product image as fallback
   */
  private getFeaturedImage(variant: PreparedProductVariant) {
    if (variant.featured_image !== null) {
      variant.featured_image.src = variant.featured_image.src.replace(
        /(^\w+:|^)\/\//,
        "//",
      ); // remove protocol
      return variant.featured_image;
    }

    let fallbackImageSrc = "";

    if (variant.images && variant.images.length > 0) {
      fallbackImageSrc = variant.images[0];
    } else if (this.scope.product) {
      fallbackImageSrc = this.scope.product.featured_image;
    }

    if (!fallbackImageSrc) {
      return null;
    }

    // remove protocol for normalization
    fallbackImageSrc = fallbackImageSrc.replace(/(^\w+:|^)\/\//, "//");

    // If variant has no image use the default product image
    if (this.scope.product) {
      const featuredImage = {
        src: fallbackImageSrc,
        position: 0,
        product_id: this.scope.product.id,
        variant_ids: [] as PreparedProductVariant["id"][],
        alt: this.scope.product.title,
        created_at: this.scope.product.created_at,
        height: 0,
        width: 0,
        id: 0,
        updated_at: this.scope.product.created_at,
      };
      return featuredImage;
    }

    throw new Error("image not found");
  }

  /**
   * prepare variant, e.g. fix missing image etc
   * @param variant
   */
  private prepareVariant(variant: PreparedProductVariant) {
    if (variant === null) {
      console.warn("Warn: Variant is null!");
      return null;
    }

    if (this.colorOption) {
      variant.images = this.getOptionImages(
        this.colorOption,
        variant.options[this.colorOption.position - 1],
      );
    } else {
      console.warn("Warn: colorOption not defined");
      variant.images = [];
    }

    variant.featured_image = this.getFeaturedImage(variant);

    if (variant.images && variant.featured_image) {
      // Remove featured image so that it does not appear twice
      const i = this.indexOfUrl(variant.images, variant.featured_image.src);
      if (i >= 0) {
        variant.images.splice(i, 1);
      }

      // add general images
      variant.images = variant.images.concat(this.getGeneralImages());
    }

    return variant;
  }
}
