/*
 * 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.ui.components;

import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.ui.Gray;
import com.intellij.ui.JBColor;
import com.intellij.ui.paint.RectanglePainter;
import com.intellij.util.ui.RegionPainter;

import java.awt.*;
import java.awt.image.*;

/**
 * @author Sergey.Malenkov
 */
class ScrollPainter extends RegionPainter.Alpha {
  private static final Color x0D = gray("mac.scroll.thumb.darcula.border", 0x0D);
  private static final Color xA6 = gray("mac.scroll.thumb.darcula.color", 0xA6);

  private static Color gray(String key, int defaultValue) {
    return new JBColor(() -> {
      int value = Registry.intValue(key, defaultValue);
      return value <= 0 ? Gray._0 : value >= 255 ? Gray._255 : Gray.get(value);
    });
  }

  static final class Track {
    static final RegionPainter<Float> DARCULA = new ScrollPainter(0, .0f, .1f, Gray.x80, null);
    static final RegionPainter<Float> DEFAULT = DARCULA;
  }

  static final class Thumb {
    static final RegionPainter<Float> DARCULA = new ScrollPainter(0, .28f, .07f, Gray.xA6, Gray.x38);
    static final RegionPainter<Float> DEFAULT = new Protected(new SubtractColor(0, .20f, .08f, Gray.x73, Gray.x91),
                                                              new ScrollPainter(0, .20f, .08f, Gray.x73, Gray.x59));

    static final class Mac {
      static final RegionPainter<Float> DARCULA = new Round(1, .35f, .20f, Gray.x80, Gray.x26);
      static final RegionPainter<Float> DEFAULT = new Round(2, .20f, .30f, Gray.x00, null);

      static final class Overlay {
        static final RegionPainter<Float> DARCULA = new Round(1, 0f, .55f, Gray.x80, Gray.x26);
        static final RegionPainter<Float> DEFAULT = new Round(2, 0f, .50f, Gray.x00, null);
      }
    }
  }

  static final class EditorThumb {
    static final RegionPainter<Float> DARCULA = new ScrollPainter(0, .33f, .12f, Gray.xA6, Gray.x1A);
    static final RegionPainter<Float> DEFAULT = new Protected(new SubtractColor(0, .25f, .15f, Gray.x80, Gray.xA6),
                                                              new ScrollPainter(0, .25f, .15f, Gray.x80, Gray.x59));

    static final class Mac {
      static final RegionPainter<Float> DARCULA = new Round(1, .35f, .20f, xA6, x0D);
      static final RegionPainter<Float> DEFAULT = Thumb.Mac.DEFAULT;

      static final class Overlay {
        static final RegionPainter<Float> DARCULA = new Round(1, 0f, .55f, xA6, x0D);
        static final RegionPainter<Float> DEFAULT = Thumb.Mac.Overlay.DEFAULT;
      }
    }
  }

  private final int myOffset;
  private final float myAlphaBase;
  private final float myAlphaDelta;
  private final Color myFillColor;
  private final Color myDrawColor;

  private ScrollPainter(int offset, float base, float delta, Color fill, Color draw) {
    myOffset = offset;
    myAlphaBase = base;
    myAlphaDelta = delta;
    myFillColor = fill;
    myDrawColor = draw;
  }

  @Override
  protected float getAlpha(Float value) {
    return value != null ? myAlphaBase + myAlphaDelta * value : 0;
  }

  @Override
  protected void paint(Graphics2D g, int x, int y, int width, int height) {
    if (myOffset > 0) {
      x += myOffset;
      y += myOffset;
      width -= myOffset + myOffset;
      height -= myOffset + myOffset;
    }
    if (width > 0 && height > 0) {
      if (myFillColor != null) {
        g.setColor(myFillColor);
        fill(g, x, y, width, height, myDrawColor != null);
      }
      if (myDrawColor != null) {
        g.setColor(myDrawColor);
        draw(g, x, y, width, height);
      }
    }
  }

  protected void fill(Graphics2D g, int x, int y, int width, int height, boolean border) {
    if (border) {
      g.fillRect(x + 1, y + 1, width - 2, height - 2);
    }
    else {
      g.fillRect(x, y, width, height);
    }
  }

  protected void draw(Graphics2D g, int x, int y, int width, int height) {
    RectanglePainter.DRAW.paint(g, x, y, width, height, null);
  }

  private static class Round extends ScrollPainter {
    private Round(int offset, float base, float delta, Color fill, Color draw) {
      super(offset, base, delta, fill, draw);
    }

    @Override
    protected void fill(Graphics2D g, int x, int y, int width, int height, boolean border) {
      RectanglePainter.FILL.paint(g, x, y, width, height, Math.min(width, height));
    }

    @Override
    protected void draw(Graphics2D g, int x, int y, int width, int height) {
      RectanglePainter.DRAW.paint(g, x, y, width, height, Math.min(width, height));
    }
  }

  private static class Protected implements RegionPainter<Float> {
    private RegionPainter<Float> myPainter;
    private RegionPainter<Float> myFallback;

    private Protected(RegionPainter<Float> painter, RegionPainter<Float> fallback) {
      myPainter = painter;
      myFallback = fallback;
    }

    @Override
    public void paint(Graphics2D g, int x, int y, int width, int height, Float value) {
      RegionPainter<Float> painter = myFallback;
      if (myPainter != null) {
        try {
          myPainter.paint(g, x, y, width, height, value);
          return;
        }
        catch (Throwable exception) {
          // do not try to use myPainter again on other systems
          if (!SystemInfo.isWindows) myPainter = null;
        }
      }
      if (painter != null) {
        painter.paint(g, x, y, width, height, value);
      }
    }
  }

  private static class SubtractColor extends ScrollPainter {
    private SubtractColor(int offset, float base, float delta, Color fill, Color draw) {
      super(offset, base, delta, fill, draw);
    }

    @Override
    protected Composite getComposite(float alpha) {
      return alpha < 1
             ? new SubtractComposite(alpha)
             : AlphaComposite.SrcOver;
    }
  }

  private static class SubtractComposite implements Composite, CompositeContext {
    private final float myAlpha;

    private SubtractComposite(float alpha) {
      myAlpha = alpha;
    }

    private int subtract(int newValue, int oldValue) {
      float value = (oldValue & 0xFF) - (newValue & 0xFF) * myAlpha;
      return value <= 0 ? 0 : (int)value;
    }

    @Override
    public CompositeContext createContext(ColorModel src, ColorModel dst, RenderingHints hints) {
      return isValid(src) && isValid(dst) ? this : AlphaComposite.SrcOver.derive(myAlpha).createContext(src, dst, hints);
    }

    private static boolean isValid(ColorModel model) {
      if (model instanceof DirectColorModel && DataBuffer.TYPE_INT == model.getTransferType()) {
        DirectColorModel dcm = (DirectColorModel)model;
        if (0x00FF0000 == dcm.getRedMask() && 0x0000FF00 == dcm.getGreenMask() && 0x000000FF == dcm.getBlueMask()) {
          return 4 != dcm.getNumComponents() || 0xFF000000 == dcm.getAlphaMask();
        }
      }
      return false;
    }

    @Override
    public void compose(Raster srcIn, Raster dstIn, WritableRaster dstOut) {
      int width = Math.min(srcIn.getWidth(), dstIn.getWidth());
      int height = Math.min(srcIn.getHeight(), dstIn.getHeight());

      int[] srcPixels = new int[width];
      int[] dstPixels = new int[width];

      for (int y = 0; y < height; y++) {
        srcIn.getDataElements(0, y, width, 1, srcPixels);
        dstIn.getDataElements(0, y, width, 1, dstPixels);
        for (int x = 0; x < width; x++) {
          int src = srcPixels[x];
          int dst = dstPixels[x];
          int a = subtract(src >> 24, dst >> 24) << 24;
          int r = subtract(src >> 16, dst >> 16) << 16;
          int g = subtract(src >> 8, dst >> 8) << 8;
          int b = subtract(src, dst);
          dstPixels[x] = a | r | g | b;
        }
        dstOut.setDataElements(0, y, width, 1, dstPixels);
      }
    }

    @Override
    public void dispose() {
    }
  }
}
