/*
 * 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.bulenkov.iconloader.util;

import com.bulenkov.iconloader.IconLoader;
import com.bulenkov.iconloader.RetinaImage;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.image.ImageFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentMap;

/**
 * @author Konstantin Bulenkov
 */
public class ImageLoader implements Serializable {
//  private static final Log LOG = Logger.getLogger("#com.intellij.util.ImageLoader");

  private static final ConcurrentMap<String, Image> ourCache = new ConcurrentSoftValueHashMap<String, Image>();

  private static class ImageDesc {
    public enum Type {
      PNG,

//      SVG {
//        @Override
//        public Image load(URL url, InputStream is, float scale) throws IOException {
//          return SVGLoader.load(url, is, scale);
//        }
//      },

      UNDEFINED;

      public Image load(URL url, InputStream stream, float scale) throws IOException {
        return ImageLoader.load(stream, (int)scale);
      }
    }

    public final String path;
    public final @Nullable Class cls; // resource class if present
    public final float scale; // initial scale factor
    public final Type type;
    public final boolean original; // path is not altered

    public ImageDesc(String path, Class cls, float scale, Type type) {
      this(path, cls, scale, type, false);
    }

    public ImageDesc(String path, Class cls, float scale, Type type, boolean original) {
      this.path = path;
      this.cls = cls;
      this.scale = scale;
      this.type = type;
      this.original = original;
    }

    @Nullable
    public Image load() throws IOException {
      String cacheKey = null;
      InputStream stream = null;
      URL url = null;
      if (cls != null) {
        //noinspection IOResourceOpenedButNotSafelyClosed
        stream = cls.getResourceAsStream(path);
        if (stream == null) return null;
      }
      if (stream == null) {
        url = new URL(path);
        URLConnection connection = url.openConnection();
        if (connection instanceof HttpURLConnection) {
          if (!original) return null;
          connection.addRequestProperty("User-Agent", "IntelliJ");

          cacheKey = path;
          Image image = ourCache.get(cacheKey);
          if (image != null) return image;
        }
        stream = connection.getInputStream();
      }
      Image image = type.load(url, stream, scale);
      if (image != null && cacheKey != null) {
        ourCache.put(cacheKey, image);
      }
      return image;
    }

    @Override
    public String toString() {
      return path + ", scale: " + scale + ", type: " + type;
    }
  }

  private static class ImageDescList extends ArrayList<ImageDesc> {
    private ImageDescList() {}

    @Nullable
    public Image load() {
      return load(ImageConverterChain.create());
    }

    @Nullable
    public Image load(@NotNull ImageConverterChain converters) {
      for (ImageDesc desc : this) {
        try {
          Image image = desc.load();
          if (image == null) continue;
//          LOG.debug("Loaded image: " + desc);
          return converters.convert(image, desc);
        }
        catch (IOException ignore) {
        }
      }
      return null;
    }

    public static ImageDescList create(@NotNull String file,
                                       @Nullable Class cls,
                                       boolean dark,
                                       boolean retina,
                                       boolean allowFloatScaling)
    {
      ImageDescList vars = new ImageDescList();
      if (retina || dark) {
        final String name = getNameWithoutExtension(file);
        final String ext = getExtension(file);

        float scale = calcScaleFactor(allowFloatScaling);

        // TODO: allow SVG images to freely scale on Retina

//        if (Registry.is("ide.svg.icon") && dark) {
//          vars.add(new ImageDesc(name + "_dark.svg", cls, UIUtil.isRetina() ? 2f : scale, ImageDesc.Type.SVG));
//        }
//
//        if (Registry.is("ide.svg.icon")) {
//          vars.add(new ImageDesc(name + ".svg", cls, UIUtil.isRetina() ? 2f : scale, ImageDesc.Type.SVG));
//        }

        if (dark && retina) {
          vars.add(new ImageDesc(name + "@2x_dark." + ext, cls, 2f, ImageDesc.Type.PNG));
        }

        if (dark) {
          vars.add(new ImageDesc(name + "_dark." + ext, cls, 1f, ImageDesc.Type.PNG));
        }

        if (retina) {
          vars.add(new ImageDesc(name + "@2x." + ext, cls, 2f, ImageDesc.Type.PNG));
        }
      }
      vars.add(new ImageDesc(file, cls, 1f, ImageDesc.Type.PNG, true));
      return vars;
    }
  }

  private interface ImageConverter {
    Image convert(@Nullable Image source, ImageDesc desc);
  }

  private static class ImageConverterChain extends ArrayList<ImageConverter> {
    private ImageConverterChain() {}

    public static ImageConverterChain create() {
      return new ImageConverterChain();
    }

    public ImageConverterChain withFilter(final ImageFilter filter) {
      return with(new ImageConverter() {
        @Override
        public Image convert(Image source, ImageDesc desc) {
          return ImageUtil.filter(source, filter);
        }
      });
    }

    public ImageConverterChain withRetina() {
      return with(new ImageConverter() {
        @Override
        public Image convert(Image source, ImageDesc desc) {
          if (source != null && UIUtil.isRetina() && desc.scale > 1) {
            return RetinaImage.createFrom(source, (int)desc.scale, ourComponent);
          }
          return source;
        }
      });
    }

    public ImageConverterChain with(ImageConverter f) {
      add(f);
      return this;
    }

    public Image convert(Image image, ImageDesc desc) {
      for (ImageConverter f : this) {
        image = f.convert(image, desc);
      }
      return image;
    }
  }

  public static final Component ourComponent = new Component() {
  };

  private static boolean waitForImage(Image image) {
    if (image == null) return false;
    if (image.getWidth(null) > 0) return true;
    MediaTracker mediatracker = new MediaTracker(ourComponent);
    mediatracker.addImage(image, 1);
    try {
      mediatracker.waitForID(1, 5000);
    }
    catch (InterruptedException ex) {
      ex.printStackTrace();
    }
    return !mediatracker.isErrorID(1);
  }

  @Nullable
  public static Image loadFromUrl(@NotNull URL url) {
    return loadFromUrl(url, true);
  }

  @Nullable
  public static Image loadFromUrl(@NotNull URL url, boolean allowFloatScaling) {
    return loadFromUrl(url, allowFloatScaling, null);
  }

  @Nullable
  public static Image loadFromUrl(@NotNull URL url, boolean allowFloatScaling, ImageFilter filter) {
    final float scaleFactor = calcScaleFactor(allowFloatScaling);

    // We can't check all 3rd party plugins and convince the authors to add @2x icons.
    // (scaleFactor > 1.0) != isRetina() => we should scale images manually.
    // Note we never scale images on Retina displays because scaling is handled by the system.
    final boolean scaleImages = (scaleFactor > 1.0f) && !UIUtil.isRetina();

    // For any scale factor > 1.0, always prefer retina images, because downscaling
    // retina images provides a better result than upscaling non-retina images.
    final boolean loadRetinaImages = UIUtil.isRetina() || scaleImages;

    return ImageDescList.create(url.toString(), null, UIUtil.isUnderDarcula(), loadRetinaImages, allowFloatScaling).load(
      ImageConverterChain.create().
        withFilter(filter).
        withRetina().
        with(new ImageConverter() {
              public Image convert(Image source, ImageDesc desc) {
                if (source != null && scaleImages /*&& desc.type != ImageDesc.Type.SVG*/) {
                  if (desc.path.contains("@2x"))
                    return scaleImage(source, scaleFactor / 2.0f);  // divide by 2.0 as Retina images are 2x the resolution.
                  else
                    return scaleImage(source, scaleFactor);
                }
                return source;
              }
        }));
  }

  private static float calcScaleFactor(boolean allowFloatScaling) {
    float scaleFactor = allowFloatScaling ? JBUI.scale(1f) : JBUI.scale(1f) > 1.5f ? 2f : 1f;
    assert scaleFactor >= 1.0f : "By design, only scale factors >= 1.0 are supported";
    return scaleFactor;
  }

  @NotNull
  private static Image scaleImage(Image image, float scale) {
    int w = image.getWidth(null);
    int h = image.getHeight(null);
    if (w <= 0 || h <= 0) {
      return image;
    }
    int width = (int)(scale * w);
    int height = (int)(scale * h);
    // Using "QUALITY" instead of "ULTRA_QUALITY" results in images that are less blurry
    // because ultra quality performs a few more passes when scaling, which introduces blurriness
    // when the scaling factor is relatively small (i.e. <= 3.0f) -- which is the case here.
    return Scalr.resize(ImageUtil.toBufferedImage(image), Scalr.Method.QUALITY, width, height);
  }

  @Nullable
  public static Image loadFromUrl(URL url, boolean dark, boolean retina) {
    return loadFromUrl(url, dark, retina, null);
  }

  @Nullable
  public static Image loadFromUrl(URL url, boolean dark, boolean retina, ImageFilter filter) {
    return ImageDescList.create(url.toString(), null, dark, retina, true).
      load(ImageConverterChain.create().withFilter(filter).withRetina());
  }

  @Nullable
  public static Image loadFromResource(@NonNls @NotNull String s) {
    Class callerClass = ReflectionUtil.getGrandCallerClass();
    if (callerClass == null) return null;
    return loadFromResource(s, callerClass);
  }

  @Nullable
  public static Image loadFromResource(@NonNls @NotNull String path, @NotNull Class aClass) {
    return ImageDescList.create(path, aClass, UIUtil.isUnderDarcula(), UIUtil.isRetina() || JBUI.scale(1.0f) >= 1.5f, true).
      load(ImageConverterChain.create().withRetina());
  }

  public static Image loadFromStream(@NotNull final InputStream inputStream) {
    return loadFromStream(inputStream, 1);
  }

  public static Image loadFromStream(@NotNull final InputStream inputStream, final int scale) {
    return loadFromStream(inputStream, scale, null);
  }

  public static Image loadFromStream(@NotNull final InputStream inputStream, final int scale, ImageFilter filter) {
    Image image = load(inputStream, scale);
    ImageDesc desc = new ImageDesc("", null, scale, ImageDesc.Type.UNDEFINED);
    return ImageConverterChain.create().withFilter(filter).withRetina().convert(image, desc);
  }

  private static Image load(@NotNull final InputStream inputStream, final int scale) {
    if (scale <= 0) throw new IllegalArgumentException("Scale must be 1 or greater");
    try {
      BufferExposingByteArrayOutputStream outputStream = new BufferExposingByteArrayOutputStream();
      try {
        byte[] buffer = new byte[1024];
        while (true) {
          final int n = inputStream.read(buffer);
          if (n < 0) break;
          outputStream.write(buffer, 0, n);
        }
      }
      finally {
        inputStream.close();
      }

      Image image = Toolkit.getDefaultToolkit().createImage(outputStream.getInternalBuffer(), 0, outputStream.size());

      waitForImage(image);

      return image;
    }
    catch (Exception ex) {
      ex.printStackTrace();
    }

    return null;
  }

  public static boolean isGoodSize(final Icon icon) {
    return IconLoader.isGoodSize(icon);
  }

  /**
   * @deprecated use {@link ImageDescList}
   */
  public static java.util.List<Pair<String, Integer>> getFileNames(@NotNull String file) {
    return getFileNames(file, false, false);
  }

  /**
   * @deprecated use {@link ImageDescList}
   */
  public static java.util.List<Pair<String, Integer>> getFileNames(@NotNull String file, boolean dark, boolean retina) {
    new UnsupportedOperationException("unsupported method").printStackTrace();
    return new ArrayList<Pair<String, Integer>>();
  }

  @NotNull
  public static String getNameWithoutExtension(@NotNull String name) {
    int i = name.lastIndexOf('.');
    if (i != -1) {
      name = name.substring(0, i);
    }
    return name;
  }

  @NotNull
  public static String getExtension(@NotNull String fileName) {
    int index = fileName.lastIndexOf('.');
    if (index < 0) return "";
    return fileName.substring(index + 1);
  }
}
