/*
 * Copyright 2013 serso aka se.solovyev
 *
 * 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.
 *
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * Contact details
 *
 * Email: se.solovyev@gmail.com
 * Site:  http://se.solovyev.org
 */

package org.solovyev.android.calculator.view;

import android.graphics.Typeface;
import android.support.annotation.NonNull;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;

import com.google.common.collect.Lists;

import org.solovyev.android.Check;
import org.solovyev.android.calculator.BaseNumberBuilder;
import org.solovyev.android.calculator.Engine;
import org.solovyev.android.calculator.LiteNumberBuilder;
import org.solovyev.android.calculator.NumberBuilder;
import org.solovyev.android.calculator.math.MathType;
import org.solovyev.android.calculator.text.TextProcessor;
import org.solovyev.android.calculator.text.TextProcessorEditorResult;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.Nonnull;

public class TextHighlighter implements TextProcessor<TextProcessorEditorResult, String> {

    private final int red;
    private final int green;
    private final int blue;
    private final boolean formatNumber;
    private final int dark;
    @Nonnull
    private final Engine engine;

    public TextHighlighter(int color, boolean formatNumber, @Nonnull Engine engine) {
        this.formatNumber = formatNumber;
        this.engine = engine;
        red = red(color);
        green = green(color);
        blue = blue(color);
        dark = isDark(red, green, blue) ? 1 : -1;
    }

    private static int blue(int color) {
        return color & 0xFF;
    }

    private static int green(int color) {
        return (color >> 8) & 0xFF;
    }

    private static int red(int color) {
        return (color >> 16) & 0xFF;
    }

    public static boolean isDark(int color) {
        return isDark(red(color), green(color), color & 0xFF);
    }

    public static boolean isDark(int red, int green, int blue) {
        final float y = 0.2126f * red + 0.7152f * green + 0.0722f * blue;
        return y < 128;
    }

    @Nonnull
    @Override
    public TextProcessorEditorResult process(@Nonnull String text) {
        final SpannableStringBuilder sb = new SpannableStringBuilder();
        final BaseNumberBuilder nb = !formatNumber ? new LiteNumberBuilder(engine) : new NumberBuilder(engine);
        final MathType.Result result = new MathType.Result();

        int offset = 0;
        int groupsCount = 0;
        int openGroupsCount = 0;

        for (int i = 0; i < text.length(); i++) {
            MathType.getType(text, i, nb.isHexMode(), result, engine);

            offset += nb.process(sb, result);

            final String match = result.match;
            switch (result.type) {
                case open_group_symbol:
                    openGroupsCount++;
                    groupsCount = Math.max(groupsCount, openGroupsCount);
                    sb.append(text.charAt(i));
                    break;
                case close_group_symbol:
                    openGroupsCount--;
                    sb.append(text.charAt(i));
                    break;
                case operator:
                    i += append(sb, match);
                    break;
                case function:
                    i += append(sb, match);
                    makeItalic(sb, i + 1 - match.length(), i + 1);
                    break;
                case constant:
                case numeral_base:
                    i += append(sb, match);
                    makeBold(sb, i + 1 - match.length(), i + 1);
                    break;
                default:
                    if (result.type == MathType.text || match.length() <= 1) {
                        sb.append(text.charAt(i));
                    } else {
                        i += append(sb, match);
                    }
            }
        }

        if (nb instanceof NumberBuilder) {
            offset += ((NumberBuilder) nb).processNumber(sb);
        }

        if (groupsCount == 0) {
            return new TextProcessorEditorResult(sb, offset);
        }
        final List<GroupSpan> groupSpans = new ArrayList<>(groupsCount);
        fillGroupSpans(sb, 0, 0, groupsCount, groupSpans);
        for (GroupSpan groupSpan : Lists.reverse(groupSpans)) {
            makeColor(sb, groupSpan.start, groupSpan.end, getColor(groupSpan.group, groupsCount));
        }
        return new TextProcessorEditorResult(sb, offset);
    }

    private int append(SpannableStringBuilder t, String match) {
        t.append(match);
        if (match.length() > 1) {
            return match.length() - 1;
        }
        return 0;
    }

    private static void makeItalic(@Nonnull SpannableStringBuilder t, int start, int end) {
        setSpan(t, new StyleSpan(Typeface.ITALIC), start, end);
    }

    private static void makeBold(@Nonnull SpannableStringBuilder t, int start, int end) {
        setSpan(t, new StyleSpan(Typeface.BOLD), start, end);
    }

    private static void makeColor(@Nonnull SpannableStringBuilder t, int start, int end, int color) {
        setSpan(t, new ForegroundColorSpan(color), start, end);
    }

    private static void setSpan(@Nonnull SpannableStringBuilder t, @NonNull Object span, int start, int end) {
        start = Math.max(0, Math.min(start, t.length()));
        end = Math.max(0, Math.min(end, t.length()));
        if (start >= end) {
            return;
        }
        t.setSpan(span, start, end, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

    private int fillGroupSpans(@Nonnull SpannableStringBuilder sb, int start, int group, int groupsCount, @Nonnull List<GroupSpan> spans) {
        for (int i = start; i < sb.length(); i++) {
            final char c = sb.charAt(i);
            if (MathType.isOpenGroupSymbol(c)) {
                i = highlightGroup(sb, i, group + 1, groupsCount, spans);
            } else if (MathType.isCloseGroupSymbol(c)) {
                return i;
            }
        }

        return sb.length();
    }

    private int highlightGroup(SpannableStringBuilder sb, int start, int group, int groupsCount, @Nonnull List<GroupSpan> spans) {
        final int end = Math.min(sb.length(), fillGroupSpans(sb, start + 1, group, groupsCount, spans));
        if (start + 1 < end) {
            spans.add(new GroupSpan(start + 1, end, group));
        }
        return end;
    }

    private int getColor(int group, int groupsCount) {
        final int offset = (int) (dark * 255 * 0.6) * group / (groupsCount + 1);
        return (0xFF << 24) | ((red + offset) << 16) | ((green + offset) << 8) | (blue + offset);
    }

    private static class GroupSpan {
        final int start;
        final int end;
        final int group;

        private GroupSpan(int start, int end, int group) {
            Check.isTrue(start < end);
            this.start = start;
            this.end = end;
            this.group = group;
        }
    }
}
