/*
 * Copyright (c) 2013, 2014 Oracle and/or its affiliates. All rights reserved. This
 * code is released under a tri EPL/GPL/LGPL license. You can use it,
 * redistribute it and/or modify it under the terms of the:
 *
 * Eclipse Public License version 1.0
 * GNU General Public License version 2
 * GNU Lesser General Public License version 2.1
 */
package org.jruby.truffle.nodes.core;

import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.UnexpectedResultException;
import org.jruby.truffle.nodes.RubyNode;
import org.jruby.truffle.nodes.control.PassthroughNode;
import org.jruby.truffle.runtime.UndefinedPlaceholder;
import org.jruby.truffle.runtime.control.RaiseException;
import org.jruby.truffle.runtime.core.RubyBasicObject;
import org.jruby.truffle.runtime.core.RubyFixnum;
import org.jruby.truffle.runtime.core.RubyRange;

public class FixnumLowerNode extends PassthroughNode {

    @CompilerDirectives.CompilationFinal private boolean hasSeenInteger = false;
    @CompilerDirectives.CompilationFinal private boolean hasSeenLong = false;
    @CompilerDirectives.CompilationFinal private boolean hasSeenIntegerRange = false;
    @CompilerDirectives.CompilationFinal private boolean hasSeenLongRange = false;
    @CompilerDirectives.CompilationFinal private boolean hasSeenUndefined = false;

    @CompilerDirectives.CompilationFinal private boolean hasNeededToLowerLongFixnum = false;
    @CompilerDirectives.CompilationFinal private boolean hasNeededToLowerLongFixnumRange = false;

    public FixnumLowerNode(RubyNode child) {
        super(child);
    }

    @Override
    public RubyBasicObject executeRubyBasicObject(VirtualFrame frame) throws UnexpectedResultException {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object execute(VirtualFrame frame) {
        final Object value = child.execute(frame);

        if (hasSeenInteger && value instanceof Integer) {
            return value;
        }

        if (hasSeenLong && value instanceof Long) {
            if (canLower((long) value)) {
                return lower((long) value);
            } else {
                return value;
            }
        }

        if (hasSeenIntegerRange && value instanceof RubyRange.IntegerFixnumRange) {
            return value;
        }

        if (hasSeenLongRange && value instanceof RubyRange.LongFixnumRange) {
            if (canLower((RubyRange.LongFixnumRange) value)) {
                return lower((RubyRange.LongFixnumRange) value);
            } else {
                return value;
            }
        }

        if (hasSeenUndefined && value instanceof UndefinedPlaceholder) {
            return value;
        }

        CompilerDirectives.transferToInterpreterAndInvalidate();

        if (value instanceof Integer) {
            hasSeenInteger = true;
            return value;
        }

        if (value instanceof Long) {
            hasSeenLong = true;
            if (canLower((long) value)) {
                return lower((long) value);
            } else {
                return value;
            }
        }

        if (value instanceof RubyRange.IntegerFixnumRange) {
            hasSeenIntegerRange = true;
            return value;
        }

        if (value instanceof RubyRange.LongFixnumRange) {
            hasSeenLongRange = true;
            if (canLower((RubyRange.LongFixnumRange) value)) {
                return lower((RubyRange.LongFixnumRange) value);
            } else {
                return value;
            }
        }

        if (value instanceof UndefinedPlaceholder) {
            hasSeenUndefined = true;
            return value;
        }

        throw new RaiseException(getContext().getCoreLibrary().typeErrorCantConvertInto(
                value, getContext().getCoreLibrary().getIntegerClass(), this));
    }

    @Override
    public int executeIntegerFixnum(VirtualFrame frame) throws UnexpectedResultException {
        try {
            if (hasNeededToLowerLongFixnum) {
                final long value = super.executeLongFixnum(frame);

                if (canLower(value)) {
                    return lower(value);
                } else {
                    throw new UnexpectedResultException(value);
                }
            } else {
                return super.executeIntegerFixnum(frame);
            }
        } catch (UnexpectedResultException e) {
            if (e.getResult() instanceof Long && canLower((long) e.getResult())) {
                hasNeededToLowerLongFixnum = true;
                return lower((long) e.getResult());
            } else if (e.getResult() instanceof RubyRange.LongFixnumRange && canLower((RubyRange.LongFixnumRange) e.getResult())) {
                hasNeededToLowerLongFixnumRange = true;
                throw new UnexpectedResultException(lower((RubyRange.LongFixnumRange) e.getResult()));
            } else {
                throw e;
            }
        }
    }

    @Override
    public long executeLongFixnum(VirtualFrame frame) throws UnexpectedResultException {
        throw new RuntimeException();
    }

    @Override
    public RubyRange.IntegerFixnumRange executeIntegerFixnumRange(VirtualFrame frame) throws UnexpectedResultException {
        try {
            if (hasNeededToLowerLongFixnumRange) {
                final RubyRange.LongFixnumRange range = super.executeLongFixnumRange(frame);

                if (canLower(range)) {
                    return lower(range);
                } else {
                    throw new UnexpectedResultException(range);
                }
            } else {
                return super.executeIntegerFixnumRange(frame);
            }
        } catch (UnexpectedResultException e) {
            if (e.getResult() instanceof Long && canLower((long) e.getResult())) {
                hasNeededToLowerLongFixnum = true;
                throw new UnexpectedResultException(lower((long) e.getResult()));
            } else if (e.getResult() instanceof RubyRange.LongFixnumRange && canLower((RubyRange.LongFixnumRange) e.getResult())) {
                hasNeededToLowerLongFixnumRange = true;
                return lower((RubyRange.LongFixnumRange) e.getResult());
            } else {
                throw e;
            }
        }
    }

    @Override
    public RubyRange.LongFixnumRange executeLongFixnumRange(VirtualFrame frame) throws UnexpectedResultException {
        throw new RuntimeException();

    }

    @Override
    public UndefinedPlaceholder executeUndefinedPlaceholder(VirtualFrame frame) throws UnexpectedResultException {
        try {
            return super.executeUndefinedPlaceholder(frame);
        } catch (UnexpectedResultException e) {
            if (e.getResult() instanceof Long && canLower((long) e.getResult())) {
                hasNeededToLowerLongFixnum = true;
                throw new UnexpectedResultException(lower((long) e.getResult()));
            } else if (e.getResult() instanceof RubyRange.LongFixnumRange && canLower((RubyRange.LongFixnumRange) e.getResult())) {
                hasNeededToLowerLongFixnumRange = true;
                throw new UnexpectedResultException(e.getResult());
            } else {
                throw e;
            }
        }
    }

    private static boolean canLower(long value) {
        return RubyFixnum.fitsIntoInteger(value);
    }

    private static int lower(long value) {
        assert canLower(value);
        return (int) value;
    }

    private static boolean canLower(RubyRange.LongFixnumRange range) {
        return canLower(range.getBegin()) && canLower(range.getEnd());
    }

    private static RubyRange.IntegerFixnumRange lower(RubyRange.LongFixnumRange range) {
        assert canLower(range);
        return new RubyRange.IntegerFixnumRange(range.getContext().getCoreLibrary().getRangeClass(), lower(range.getBegin()), lower(range.getEnd()), range.doesExcludeEnd());
    }

}
