/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * Licensed 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.
 */
package com.intellij.openapi.editor.impl;

import com.intellij.Patches;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.impl.view.FontLayoutService;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.TIntHashSet;
import org.intellij.lang.annotations.JdkConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import sun.font.FontDesignMetrics;

import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.TextAttribute;
import java.io.File;
import java.io.FilenameFilter;
import java.util.*;
import java.util.List;

/**
 * @author max
 */
public class FontInfo {
  private static final Logger LOG = Logger.getInstance(FontInfo.class);
  
  private static final boolean USE_ALTERNATIVE_CAN_DISPLAY_PROCEDURE = Registry.is("ide.mac.fix.font.fallback");
  private static final FontRenderContext DEFAULT_CONTEXT = new FontRenderContext(null, false, false);
  private static final Font DUMMY_FONT = new Font(null);

  private final Font myFont;
  private final int mySize;
  @JdkConstants.FontStyle private final int myStyle;
  private final boolean myUseLigatures;
  private final TIntHashSet mySafeCharacters = new TIntHashSet();
  private final FontRenderContext myContext;
  private FontMetrics myFontMetrics = null;

  /**
   * @deprecated Use {@link #FontInfo(String, int, int, boolean, FontRenderContext)} instead.
   */
  public FontInfo(final String familyName, final int size, @JdkConstants.FontStyle int style) {
    this(familyName, size, style, style, false, null);
  }

  /**
   * @deprecated Use {@link #FontInfo(String, int, int, boolean, FontRenderContext)} instead.
   */
  public FontInfo(final String familyName, final int size, @JdkConstants.FontStyle int style, boolean useLigatures) {
    this(familyName, size, style, useLigatures, null);
  }

  /**
   * To get valid font metrics from this {@link FontInfo} instance, pass valid {@link FontRenderContext} here as a parameter.
   */
  public FontInfo(final String familyName, final int size, @JdkConstants.FontStyle int style, boolean useLigatures,
                  FontRenderContext fontRenderContext) {
    this(familyName, size, style, style, useLigatures, fontRenderContext);
  }

  FontInfo(final String familyName, final int size,
           @JdkConstants.FontStyle int style, @JdkConstants.FontStyle int realStyle, boolean useLigatures, FontRenderContext context) {
    mySize = size;
    myStyle = style;
    myUseLigatures = useLigatures;
    Font font = new Font(familyName, style, size);
    myFont = useLigatures ? getFontWithLigaturesEnabled(font, realStyle) : font;
    myContext = context;
  }

  @NotNull
  private static Font getFontWithLigaturesEnabled(Font font, @JdkConstants.FontStyle int fontStyle) {
    if (Patches.JDK_BUG_ID_7162125) {
      // Ligatures don't work on Mac for fonts loaded natively, so we need to locate and load font manually
      String familyName = font.getFamily();
      File fontFile = findFileForFont(familyName, fontStyle);
      if (fontFile == null) {
        LOG.info(font + "(style=" + fontStyle + ") not located");
        return font;
      }
      LOG.info(font + "(style=" + fontStyle + ") located at " + fontFile);
      try {
        font = Font.createFont(Font.TRUETYPE_FONT, fontFile).deriveFont(fontStyle, font.getSize());
      }
      catch (Exception e) {
        LOG.warn("Couldn't load font", e);
        return font;
      }
    }
    return font.deriveFont(Collections.singletonMap(TextAttribute.LIGATURES, TextAttribute.LIGATURES_ON));
  }

  private static final Comparator<File> BY_NAME = Comparator.comparing(File::getName);

  @Nullable
  private static File findFileForFont(@NotNull String familyName, int style) {
    File fontFile = doFindFileForFont(familyName, style);
    if (fontFile == null && style != Font.PLAIN) fontFile = doFindFileForFont(familyName, Font.PLAIN);
    if (fontFile == null) fontFile = doFindFileForFont(familyName, -1);
    return fontFile;
  }

  @Nullable
  private static File doFindFileForFont(@NotNull String familyName, final int style) {
    final String normalizedFamilyName = familyName.toLowerCase(Locale.getDefault()).replace(" ", "");
    FilenameFilter filter = (file, name) -> {
      String normalizedName = name.toLowerCase(Locale.getDefault());
      return normalizedName.startsWith(normalizedFamilyName) &&
             (normalizedName.endsWith(".otf") || normalizedName.endsWith(".ttf")) &&
             (style == -1 || style == getFontStyle(normalizedName));
    };
    List<File> files = new ArrayList<>();
    
    File[] userFiles = new File(System.getProperty("user.home"), "Library/Fonts").listFiles(filter);
    if (userFiles != null) files.addAll(Arrays.asList(userFiles));
    
    File[] localFiles = new File("/Library/Fonts").listFiles(filter);
    if (localFiles != null) files.addAll(Arrays.asList(localFiles));
    
    if (files.isEmpty()) return null;
    
    if (style == Font.PLAIN) {
      // prefer font containing 'regular' in its name
      List<File> regulars = ContainerUtil.filter(files, file -> file.getName().toLowerCase(Locale.getDefault()).contains("regular"));
      if (!regulars.isEmpty()) return Collections.min(regulars, BY_NAME);
    }
    
    return Collections.min(files, BY_NAME);
  }

  private static int getFontStyle(@NotNull String fontFileNameLowercase) {
    String baseName = fontFileNameLowercase.substring(0, fontFileNameLowercase.length() - 4);
    if (baseName.endsWith("-it")) return Font.ITALIC;
    else if (baseName.endsWith("-boldit")) return Font.BOLD | Font.ITALIC;
    else return ComplementaryFontsRegistry.getFontStyle(fontFileNameLowercase);
  }

  public boolean canDisplay(int codePoint) {
    try {
      if (codePoint < 128) return true;
      if (mySafeCharacters.contains(codePoint)) return true;
      if (canDisplayImpl(codePoint)) {
        mySafeCharacters.add(codePoint);
        return true;
      }
      return false;
    }
    catch (Exception e) {
      // JRE has problems working with the font. Just skip.
      return false;
    }
  }

  private boolean canDisplayImpl(int codePoint) {
    if (!Character.isValidCodePoint(codePoint)) return false;
    if (USE_ALTERNATIVE_CAN_DISPLAY_PROCEDURE) {
      return myFont.createGlyphVector(DEFAULT_CONTEXT, new String(new int[]{codePoint}, 0, 1)).getGlyphCode(0) > 0;
    }
    else {
      return myFont.canDisplay(codePoint);
    }
  }

  public Font getFont() {
    return myFont;
  }

  public int charWidth(int codePoint) {
    final FontMetrics metrics = fontMetrics();
    return FontLayoutService.getInstance().charWidth(metrics, codePoint);
  }

  public float charWidth2D(int codePoint) {
    FontMetrics metrics = fontMetrics();
    return FontLayoutService.getInstance().charWidth2D(metrics, codePoint);
  }

  public synchronized FontMetrics fontMetrics() {
    if (myFontMetrics == null) {
      myFontMetrics = FontDesignMetrics.getMetrics(myFont, myContext == null ? getFontRenderContext(null) : myContext);
    }
    return myFontMetrics;
  }

  public static FontRenderContext getFontRenderContext(Component component) {
    if (component == null) {
        return DEFAULT_CONTEXT;
    }
    return component.getFontMetrics(DUMMY_FONT).getFontRenderContext();
  }

  public int getSize() {
    return mySize;
  }

  @JdkConstants.FontStyle
  public int getStyle() {
    return myStyle;
  }

  public boolean areLigaturesEnabled() {
    return myUseLigatures;
  }

  public FontRenderContext getFontRenderContext() {
    return myContext;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    FontInfo fontInfo = (FontInfo)o;

    if (!myFont.equals(fontInfo.myFont)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    return myFont.hashCode();
  }
}
