import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.util.Hashtable;
import java.net.URL;
import java.net.MalformedURLException;
import java.io.*;

/**
 * @author Josef Jelinek
 * @version 3.5b
 */

public final class AnimCube extends Applet implements Runnable, MouseListener, MouseMotionListener {
  // external configuration
  private final Hashtable config = new Hashtable();
  // background colors
  private Color bgColor;
  private Color bgColor2;
  private Color hlColor;
  private Color textColor;
  private Color buttonBgColor;
  // cube colors
  private final Color[] colors = new Color[24];
  // cube facelets
  private final int[][] cube = new int[6][9];
  private final int[][] initialCube = new int[6][9];
  // normal vectors
  private static final double[][] faceNormals = {
    { 0.0, -1.0,  0.0}, // U
    { 0.0,  1.0,  0.0}, // D
    { 0.0,  0.0, -1.0}, // F
    { 0.0,  0.0,  1.0}, // B
    {-1.0,  0.0,  0.0}, // L
    { 1.0,  0.0,  0.0}  // R
  };
  // vertex co-ordinates
  private static final double[][] cornerCoords = {
    {-1.0, -1.0, -1.0}, // UFL
    { 1.0, -1.0, -1.0}, // UFR
    { 1.0, -1.0,  1.0}, // UBR
    {-1.0, -1.0,  1.0}, // UBL
    {-1.0,  1.0, -1.0}, // DFL
    { 1.0,  1.0, -1.0}, // DFR
    { 1.0,  1.0,  1.0}, // DBR
    {-1.0,  1.0,  1.0}  // DBL
  };
  // vertices of each face
  private static final int[][] faceCorners = {
    {0, 1, 2, 3}, // U: UFL UFR UBR UBL
    {4, 7, 6, 5}, // D: DFL DBL DBR DFR
    {0, 4, 5, 1}, // F: UFL DFL DFR UFR
    {2, 6, 7, 3}, // B: UBR DBR DBL UBL
    {0, 3, 7, 4}, // L: UFL UBL DBL DFL
    {1, 5, 6, 2}  // R: UFR DFR DBR UBR
  };
  // corresponding corners on the opposite face
  private static final int[][] oppositeCorners = {
    {0, 3, 2, 1}, // U->D
    {0, 3, 2, 1}, // D->U
    {3, 2, 1, 0}, // F->B
    {3, 2, 1, 0}, // B->F
    {0, 3, 2, 1}, // L->R
    {0, 3, 2, 1}, // R->L
  };
  // faces adjacent to each face
  private static final int[][] adjacentFaces = {
    {2, 5, 3, 4}, // U: F R B L
    {4, 3, 5, 2}, // D: L B R F
    {4, 1, 5, 0}, // F: L D R U
    {5, 1, 4, 0}, // B: R D L U
    {0, 3, 1, 2}, // L: U B D F
    {2, 1, 3, 0}  // R: F D B U
  };
  // current twisted layer
  private int twistedLayer;
  private int twistedMode;
  // directions of facelet cycling for all faces
  private static final int[] faceTwistDirs = {1, 1, -1, -1, -1, -1};
  // initial observer co-ordinate axes (view)
  private final double[] eye = {0.0, 0.0, -1.0};
  private final double[] eyeX = {1.0, 0.0, 0.0}; // (sideways)
  private final double[] eyeY = new double[3]; // (vertical)
  private final double[] initialEye = new double[3];
  private final double[] initialEyeX = new double[3];
  private final double[] initialEyeY = new double[3];
  // angle of rotation of the twistedLayer
  private double currentAngle; // edited angle of twisted layer
  private double originalAngle; // angle of twisted layer
  // animation speed
  private int speed;
  private int doubleSpeed;
  // current state of the program
  private boolean natural = true; // cube is compact, no layer is twisted
  private boolean toTwist; // layer can be twisted
  private boolean interrupted; // thread was interrupted
  private boolean restarted; // animation was stopped
  private boolean mirrored; // mirroring of the cube view
  private boolean editable; // editation of the cube with a mouse
  private boolean twisting; // a user twists a cube layer
  private boolean spinning; // an animation twists a cube layer
  private boolean animating; // animation run
  private boolean dragging; // progress bar is controlled
  private boolean demo; // demo mode
  private int persp; // perspective deformation
  private double scale; // cube scale
  private int align; // cube alignment (top, center, bottom)
  private boolean hint;
  private double faceShift;
  // move sequence data
  private int[][] move;
  private int[][] demoMove;
  private int curMove;
  private int movePos;
  private int moveDir;
  private boolean moveOne;
  private boolean moveAnimated;
  private int metric;
  private String[] infoText;
  private int curInfoText;
  // state of buttons
  private int buttonBar; // button bar mode
  private int buttonHeight;
  private boolean drawButtons = true;
  private boolean pushed;
  private int buttonPressed = -1;
  private int progressHeight = 6;
  private int textHeight;
  private int moveText;
  private boolean outlined = true;
  // transformation tables for compatibility with Lars's applet
  private static final int[] posFaceTransform = {3, 2, 0, 5, 1, 4};
  private static final int[][] posFaceletTransform = {
    {6, 3, 0, 7, 4, 1, 8, 5, 2}, // B +27
    {2, 5, 8, 1, 4, 7, 0, 3, 6}, // F +18
    {0, 1, 2, 3, 4, 5, 6, 7, 8}, // U +0
    {0, 1, 2, 3, 4, 5, 6, 7, 8}, // R +45
    {6, 3, 0, 7, 4, 1, 8, 5, 2}, // D +9
    {0, 1, 2, 3, 4, 5, 6, 7, 8}  // L +36
  };
  // buffer to store hexa-digits
  private final int[] hex = new int[6];

  public void init() {
    // register to receive all mouse events
    addMouseListener(this);
    addMouseMotionListener(this);
    // setup colors
    colors[0] = new Color(255, 128, 64);   // 0 - light orange
    colors[1] = new Color(255, 0, 0);      // 1 - pure red
    colors[2] = new Color(0, 255, 0);      // 2 - pure green
    colors[3] = new Color(0, 0, 255);      // 3 - pure blue
    colors[4] = new Color(153, 153, 153);  // 4 - white grey
    colors[5] = new Color(170, 170, 68);   // 5 - yellow grey
    colors[6] = new Color(187, 119, 68);   // 6 - orange grey
    colors[7] = new Color(153, 68, 68);    // 7 - red grey
    colors[8] = new Color(68, 119, 68);    // 8 - green grey
    colors[9] = new Color(0, 68, 119);     // 9 - blue grey
    colors[10] = new Color(255, 255, 255); // W - white
    colors[11] = new Color(255, 255, 0);   // Y - yellow
    colors[12] = new Color(255, 96, 32);   // O - orange
    colors[13] = new Color(208, 0, 0);     // R - red
    colors[14] = new Color(0, 144, 0);     // G - green
    colors[15] = new Color(32, 64, 208);   // B - blue
    colors[16] = new Color(176, 176, 176); // L - light gray
    colors[17] = new Color(80, 80, 80);    // D - dark gray
    colors[18] = new Color(255, 0, 255);   // M - magenta
    colors[19] = new Color(0, 255, 255);   // C - cyan
    colors[20] = new Color(255, 160, 192); // P - pink
    colors[21] = new Color(32, 255, 16);   // N - light green
    colors[22] = new Color(0, 0, 0);       // K - black
    colors[23] = new Color(128, 128, 128); // . - gray
    // create animation thread
    animThread = new Thread(this, "Cube Animator");
    animThread.start();
    // setup default configuration
    String param = getParameter("config");
    if (param != null) {
      try {
        URL url = new URL(getDocumentBase(), param);
        InputStream input = url.openStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(input));
        String line = reader.readLine();
        while (line != null) {
          int pos = line.indexOf('=');
          if (pos > 0) {
            String key = line.substring(0, pos).trim();
            String value = line.substring(pos + 1).trim();
            config.put(key, value);
          }
          line = reader.readLine();
        }
        reader.close();
      }
      catch (MalformedURLException ex) {
        System.err.println("Malformed URL: " + param + ": " + ex);
      }
      catch (IOException ex) {
        System.err.println("Input error: " + param + ": " + ex);
      }
    }
    // setup window background color
    param = getParameter("bgcolor");
    if (param != null && param.length() == 6) {
      for (int i = 0; i < 6; i++) {
        for (int j = 0; j < 16; j++) {
          if (Character.toLowerCase(param.charAt(i)) == "0123456789abcdef".charAt(j)) {
            hex[i] = j;
            break;
          }
        }
      }
      bgColor = new Color(hex[0] * 16 + hex[1], hex[2] * 16 + hex[3], hex[4] * 16 + hex[5]);
    }
    else
      bgColor = Color.gray;
    // setup button bar background color
    param = getParameter("butbgcolor");
    if (param != null && param.length() == 6) {
      for (int i = 0; i < 6; i++) {
        for (int j = 0; j < 16; j++) {
          if (Character.toLowerCase(param.charAt(i)) == "0123456789abcdef".charAt(j)) {
            hex[i] = j;
            break;
          }
        }
      }
      buttonBgColor = new Color(hex[0] * 16 + hex[1], hex[2] * 16 + hex[3], hex[4] * 16 + hex[5]);
    }
    else
      buttonBgColor = bgColor;
    // custom colors
    param = getParameter("colors");
    if (param != null) {
      for (int k = 0; k < 10 && k < param.length() / 6; k++) {
        for (int i = 0; i < 6; i++) {
          for (int j = 0; j < 16; j++) {
            if (Character.toLowerCase(param.charAt(k * 6 + i)) == "0123456789abcdef".charAt(j)) {
              hex[i] = j;
              break;
            }
          }
        }
        colors[k] = new Color(hex[0] * 16 + hex[1], hex[2] * 16 + hex[3], hex[4] * 16 + hex[5]);
      }
    }
    // clean the cube
    for (int i = 0; i < 6; i++)
      for (int j = 0; j < 9; j++)
        cube[i][j] = i + 10;
    String initialPosition = "lluu";
    // setup color configuration of the solved cube
    param = getParameter("colorscheme");
    if (param != null && param.length() == 6) {
      for (int i = 0; i < 6; i++) { // udfblr
        int color = 23;
        for (int j = 0; j < 23; j++) {
          if (Character.toLowerCase(param.charAt(i)) == "0123456789wyorgbldmcpnk".charAt(j)) {
            color = j;
            break;
          }
        }
        for (int j = 0; j < 9; j++)
          cube[i][j] = color;
      }
    }
    // setup facelets - compatible with Lars's applet
    param = getParameter("pos");
    if (param != null && param.length() == 54) {
      initialPosition = "uuuuff";
      if (bgColor == Color.gray)
        bgColor = Color.white;
      for (int i = 0; i < 6; i++) {
        int ti = posFaceTransform[i];
        for (int j = 0; j < 9; j++) {
          int tj = posFaceletTransform[i][j];
          cube[ti][tj] = 23;
          for (int k = 0; k < 14; k++) {
            // "abcdefgh" ~ "gbrwoyld"
            if (param.charAt(i * 9 + j) == "DFECABdfecabgh".charAt(k)) {
              cube[ti][tj] = k + 4;
              break;
            }
          }
        }
      }
    }
    // setup color facelets
    param = getParameter("facelets");
    if (param != null && param.length() == 54) {
      for (int i = 0; i < 6; i++) {
        for (int j = 0; j < 9; j++) {
          cube[i][j] = 23;
          for (int k = 0; k < 23; k++) {
            if (Character.toLowerCase(param.charAt(i * 9 + j)) == "0123456789wyorgbldmcpnk".charAt(k)) {
              cube[i][j] = k;
              break;
            }
          }
        }
      }
    }
    // setup move sequence (and info texts)
    param = getParameter("move");
    move = (param == null ? new int[0][0] : getMove(param, true));
    movePos = 0;
    curInfoText = -1;
    // setup initial move sequence
    param = getParameter("initmove");
    if (param != null) {
      int[][] initialMove = param.equals("#") ? move : getMove(param, false);
      if (initialMove.length > 0)
        doMove(cube, initialMove[0], 0, initialMove[0].length, false);
    }
    // setup initial reversed move sequence
    param = getParameter("initrevmove");
    if (param != null) {
      int[][] initialReversedMove = param.equals("#") ? move : getMove(param, false);
      if (initialReversedMove.length > 0)
        doMove(cube, initialReversedMove[0], 0, initialReversedMove[0].length, true);
    }
    // setup initial reversed move sequence
    param = getParameter("demo");
    if (param != null) {
      demoMove = param.equals("#") ? move : getMove(param, true);
      if (demoMove.length > 0 && demoMove[0].length > 0)
        demo = true;
    }
    // setup initial cube position
    param = getParameter("position");
    vNorm(vMul(eyeY, eye, eyeX));
    if (param == null)
      param = initialPosition;
    double pi12 = Math.PI / 12;
    for (int i = 0; i < param.length(); i++) {
      double angle = pi12;
      switch (Character.toLowerCase(param.charAt(i))) {
       case 'd':
        angle = -angle;
       case 'u':
        vRotY(eye, angle);
        vRotY(eyeX, angle);
        break;
       case 'f':
        angle = -angle;
       case 'b':
        vRotZ(eye, angle);
        vRotZ(eyeX, angle);
        break;
       case 'l':
        angle = -angle;
       case 'r':
        vRotX(eye, angle);
        vRotX(eyeX, angle);
        break;
      }
    }
    vNorm(vMul(eyeY, eye, eyeX)); // fix eyeY
    // setup quarter-turn speed and double-turn speed
    speed = 0;
    doubleSpeed = 0;
    param = getParameter("speed");
    if (param != null)
      for (int i = 0; i < param.length(); i++)
        if (param.charAt(i) >= '0' && param.charAt(i) <= '9')
          speed = speed * 10 + (int)param.charAt(i) - '0';
    param = getParameter("doublespeed");
    if (param != null)
      for (int i = 0; i < param.length(); i++)
        if (param.charAt(i) >= '0' && param.charAt(i) <= '9')
          doubleSpeed = doubleSpeed * 10 + (int)param.charAt(i) - '0';
    if (speed == 0)
      speed = 10;
    if (doubleSpeed == 0)
      doubleSpeed = speed * 3 / 2;
    // perspective deformation
    persp = 0;
    param = getParameter("perspective");
    if (param == null)
      persp = 2;
    else
      for (int i = 0; i < param.length(); i++)
        if (param.charAt(i) >= '0' && param.charAt(i) <= '9')
          persp = persp * 10 + (int)param.charAt(i) - '0';
    // cube scale
    int intscale = 0;
    param = getParameter("scale");
    if (param != null)
      for (int i = 0; i < param.length(); i++)
        if (param.charAt(i) >= '0' && param.charAt(i) <= '9')
          intscale = intscale * 10 + (int)param.charAt(i) - '0';
    scale = 1.0 / (1.0 + intscale / 10.0);
    // hint displaying
    hint = false;
    param = getParameter("hint");
    if (param != null) {
      hint = true;
      faceShift = 0.0;
      for (int i = 0; i < param.length(); i++)
        if (param.charAt(i) >= '0' && param.charAt(i) <= '9')
          faceShift = faceShift * 10 + (int)param.charAt(i) - '0';
      if (faceShift < 1.0)
        hint = false;
      else
        faceShift /= 10.0;
    }
    // appearance and configuration of the button bar
    buttonBar = 1;
    buttonHeight = 13;
    progressHeight = move.length == 0 ? 0 : 6;
    param = getParameter("buttonbar");
    if ("0".equals(param)) {
      buttonBar = 0;
      buttonHeight = 0;
      progressHeight = 0;
    }
    else if ("1".equals(param))
      buttonBar = 1;
    else if ("2".equals(param) || move.length == 0) {
      buttonBar = 2;
      progressHeight = 0;
    }
    // whether the cube can be edited with mouse
    param = getParameter("edit");
    if ("0".equals(param))
      editable = false;
    else
      editable = true;
    // displaying the textual representation of the move
    param = getParameter("movetext");
    if ("1".equals(param))
      moveText = 1;
    else if ("2".equals(param))
      moveText = 2;
    else if ("3".equals(param))
      moveText = 3;
    else if ("4".equals(param))
      moveText = 4;
    else
      moveText = 0;
    // how texts are displayed
    param = getParameter("fonttype");
    if (param == null || "1".equals(param))
      outlined = true;
    else
      outlined = false;
    // metric
    metric = 0;
    param = getParameter("metric");
    if (param != null) {
      if ("1".equals(param)) // quarter-turn
        metric = 1;
      else if ("2".equals(param)) // face-turn
        metric = 2;
      else if ("3".equals(param)) // slice-turn
        metric = 3;
    }
    // metric
    align = 1;
    param = getParameter("align");
    if (param != null) {
      if ("0".equals(param)) // top
        align = 0;
      else if ("1".equals(param)) // center
        align = 1;
      else if ("2".equals(param)) // bottom
        align = 2;
    }
    // setup initial values
    for (int i = 0; i < 6; i++)
      for (int j = 0; j < 9; j++)
        initialCube[i][j] = cube[i][j];
    for (int i = 0; i < 3; i++) {
      initialEye[i] = eye[i];
      initialEyeX[i] = eyeX[i];
      initialEyeY[i] = eyeY[i];
    }
    // setup colors (contrast)
    int red = bgColor.getRed();
    int green = bgColor.getGreen();
    int blue = bgColor.getBlue();
    int average = (red * 299 + green * 587 + blue * 114) / 1000;
    if (average < 128) {
      textColor = Color.white;
      hlColor = bgColor.brighter();
      hlColor = new Color(hlColor.getBlue(), hlColor.getRed(), hlColor.getGreen());
    }
    else {
      textColor = Color.black;
      hlColor = bgColor.darker();
      hlColor = new Color(hlColor.getBlue(), hlColor.getRed(), hlColor.getGreen());
    }
    bgColor2 = new Color(red / 2, green / 2, blue / 2);
    curInfoText = -1;
    if (demo)
      startAnimation(-1);
  } // init()

  public String getParameter(String name) {
    String parameter = super.getParameter(name);
    if (parameter == null)
      return (String)config.get(name);
    return parameter;
  }

  private static final int[] moveModes = {
    0, 0, 0, 0, 0, 0, // UDFBLR
    1, 1, 1,          // ESM
    3, 3, 3, 3, 3, 3, // XYZxyz
    2, 2, 2, 2, 2, 2  // udfblr
  };
  private static final int[] moveCodes = {
    0, 1, 2, 3, 4, 5, // UDFBLR
    1, 2, 4,          // ESM
    5, 2, 0, 5, 2, 0, // XYZxyz
    0, 1, 2, 3, 4, 5  // udfblr
  };

  private int[][] getMove(String sequence, boolean info) {
    if (info) {
      int inum = 0;
      int pos = sequence.indexOf('{');
      while (pos != -1) {
        inum++;
        pos = sequence.indexOf('{', pos + 1);
      }
      if (infoText == null) {
        curInfoText = 0;
        infoText = new String[inum];
      }
      else {
        String[] infoText2 = new String[infoText.length + inum];
        for (int i = 0; i < infoText.length; i++)
          infoText2[i] = infoText[i];
        curInfoText = infoText.length;
        infoText = infoText2;
      }
    }
    int num = 1;
    int pos = sequence.indexOf(';');
    while (pos != -1) {
      num++;
      pos = sequence.indexOf(';', pos + 1);
    }
    int[][] move = new int[num][];
    int lastPos = 0;
    pos = sequence.indexOf(';');
    num = 0;
    while (pos != -1) {
      move[num++] = getMovePart(sequence.substring(lastPos, pos), info);
      lastPos = pos + 1;
      pos = sequence.indexOf(';', lastPos);
    }
    move[num] = getMovePart(sequence.substring(lastPos), info);
    return move;
  }

  private static final char[] modeChar = {'m', 't', 'c', 's', 'a'};

  private int[] getMovePart(String sequence, boolean info) {
    int length = 0;
    int[] move = new int[sequence.length()]; // overdimmensioned
    for (int i = 0; i < sequence.length(); i++) {
      if (sequence.charAt(i) == '.') {
        move[length] = -1;
        length++;
      }
      else if (sequence.charAt(i) == '{') {
        i++;
        String s = "";
        while (i < sequence.length()) {
          if (sequence.charAt(i) == '}')
            break;
          if (info)
            s += sequence.charAt(i);
          i++;
        }
        if (info) {
          infoText[curInfoText] = s;
          move[length] = 1000 + curInfoText;
          curInfoText++;
          length++;
        }
      }
      else {
        for (int j = 0; j < 21; j++) {
          if (sequence.charAt(i) == "UDFBLRESMXYZxyzudfblr".charAt(j)) {
            i++;
            int mode = moveModes[j];
            move[length] = moveCodes[j] * 24;
            if (i < sequence.length()) {
              if (moveModes[j] == 0) { // modifiers for basic characters UDFBLR
                for (int k = 0; k < modeChar.length; k++) {
                  if (sequence.charAt(i) == modeChar[k]) {
                    mode = k + 1;
                    i++;
                    break;
                  }
                }
              }
            }
            move[length] += mode * 4;
            if (i < sequence.length()) {
              if (sequence.charAt(i) == '1')
                i++;
              else if (sequence.charAt(i) == '\'' || sequence.charAt(i) == '3') {
                move[length] += 2;
                i++;
              }
              else if (sequence.charAt(i) == '2') {
                i++;
                if (i < sequence.length() && sequence.charAt(i) == '\'') {
                  move[length] += 3;
                  i++;
                }
                else
                  move[length] += 1;
              }
            }
            length++;
            i--;
            break;
          }
        }
      }
    }
    int[] returnMove = new int[length];
    for (int i = 0; i < length; i++)
      returnMove[i] = move[i];
    return returnMove;
  }

  private String moveText(int[] move, int start, int end) {
    if (start >= move.length)
      return "";
    String s = "";
    for (int i = start; i < end; i++)
      s += turnText(move, i);
    return s;
  }

  private static final String[][][] turnSymbol = {
    { // "standard" notation
      {"U", "D", "F", "B", "L", "R"},
      {"Um", "Dm", "Fm", "Bm", "Lm", "Rm"},
      {"Ut", "Dt", "Ft", "Bt", "Lt", "Rt"},
      {"Uc", "Dc", "Fc", "Bc", "Lc", "Rc"},
      {"Us", "Ds", "Fs", "Bs", "Ls", "Rs"},
      {"Ua", "Da", "Fa", "Ba", "La", "Ra"}
    },
    { // "reduced" notation
      {"U", "D", "F", "B", "L", "R"},
      {"~E", "E", "S", "~S", "M", "~M"},
      {"u", "d", "f", "b", "l", "r"},
      {"Z", "~Z", "Y", "~Y", "~X", "X"},
      {"Us", "Ds", "Fs", "Bs", "Ls", "Rs"},
      {"Ua", "Da", "Fa", "Ba", "La", "Ra"}
    },
    { // "reduced" notation - swapped Y and Z
      {"U", "D", "F", "B", "L", "R"},
      {"~E", "E", "S", "~S", "M", "~M"},
      {"u", "d", "f", "b", "l", "r"},
      {"Y", "~Y", "Z", "~Z", "~X", "X"},
      {"Us", "Ds", "Fs", "Bs", "Ls", "Rs"},
      {"Ua", "Da", "Fa", "Ba", "La", "Ra"}
    },
    { // another reduced notation
      {"U", "D", "F", "B", "L", "R"},
      {"u", "d", "f", "b", "l", "r"},
      {"Uu", "Dd", "Ff", "Bb", "Ll", "Rr"},
      {"QU", "QD", "QF", "QB", "QL", "QR"},
      {"UD'", "DU'", "FB'", "BF'", "LR'", "RL'"},
      {"UD", "DU", "FB", "BF", "LR", "RL"}
    }
  };
  private static final String[] modifierStrings = {"", "2", "'", "2'"};

  private String turnText(int[] move, int pos) {
    if (pos >= move.length)
      return "";
    if (move[pos] >= 1000)
      return "";
    if (move[pos] == -1)
      return ".";
    String s = turnSymbol[moveText - 1][move[pos] / 4 % 6][move[pos] / 24];
    if (s.charAt(0) == '~')
      return s.substring(1) + modifierStrings[(move[pos] + 2) % 4];
    return s + modifierStrings[move[pos] % 4];
  }

  private static final String[] metricChar = {"", "q", "f", "s"};

  private static int realMoveLength(int[] move) {
    int length = 0;
    for (int i = 0; i < move.length; i++)
      if (move[i] < 1000)
        length++;
    return length;
  }

  private static int realMovePos(int[] move, int pos) {
    int rpos = 0;
    for (int i = 0; i < pos; i++)
      if (move[i] < 1000)
        rpos++;
    return rpos;
  }

  private static int arrayMovePos(int[] move, int realPos) {
    int pos = 0;
    int rpos = 0;
    while (true) {
      while (pos < move.length && move[pos] >= 1000)
        pos++;
      if (rpos == realPos)
        break;
      if (pos < move.length) {
        rpos++;
        pos++;
      }
    }
    return pos;
  }

  private int moveLength(int[] move, int end) {
    int length = 0;
    for (int i = 0; i < move.length && (i < end || end < 0); i++)
      length += turnLength(move[i]);
    return length;
  }

  private int turnLength(int turn) {
    if (turn < 0 || turn >= 1000)
      return 0;
    int modifier = turn % 4;
    int mode = turn / 4 % 6;
    int n = 1;
    switch (metric) {
     case 1: // quarter-turn metric
      if (modifier == 1 || modifier == 3)
        n *= 2;
     case 2: // face-turn metric
      if (mode == 1 || mode == 4 || mode == 5)
        n *= 2;
     case 3: // slice-turn metric
      if (mode == 3)
        n = 0;
    }
    return n;
  }

  private void initInfoText(int[] move) {
    if (move.length > 0 && move[0] >= 1000)
      curInfoText = move[0] - 1000;
    else
      curInfoText = -1;
  }

  private void doMove(int[][] cube, int[] move, int start, int length, boolean reversed) {
    int position = reversed ? start + length : start;
    while (true) {
      if (reversed) {
        if (position <= start)
          break;
        position--;
      }
      if (move[position] >= 1000) {
        curInfoText = reversed ? -1 : move[position] - 1000;
      }
      else if (move[position] >= 0) {
        int modifier = move[position] % 4 + 1;
        int mode = move[position] / 4 % 6;
        if (modifier == 4) // reversed double turn
          modifier = 2;
        if (reversed)
          modifier = 4 - modifier;
        twistLayers(cube, move[position] / 24, modifier, mode);
      }
      if (!reversed) {
        position++;
        if (position >= start + length)
          break;
      }
    }
  }

  private Thread animThread = null; // thread to perform the animation

  private void startAnimation(int mode) {
    synchronized (animThread) {
      stopAnimation();
      if (!demo && (move.length == 0 || move[curMove].length == 0))
        return;
      if (demo && (demoMove.length == 0 || demoMove[0].length == 0))
        return;
      moveDir = 1;
      moveOne = false;
      moveAnimated = true;
      switch (mode) {
       case 0: // play forward
        break;
       case 1: // play backward
        moveDir = -1;
        break;
       case 2: // step forward
        moveOne = true;
        break;
       case 3: // step backward
        moveDir = -1;
        moveOne = true;
        break;
       case 4: // fast forward
        moveAnimated = false;
        break;
      }
      //System.err.println("start: notify");
      animThread.notify();
    }
  }

  public void stopAnimation() {
    synchronized (animThread) {
      restarted = true;
      //System.err.println("stop: notify");
      animThread.notify();
      try {
        //System.err.println("stop: wait");
        animThread.wait();
        //System.err.println("stop: run");
      }
      catch (InterruptedException e) {
        interrupted = true;
      }
      restarted = false;
    }
  }

  public void run() {
    synchronized (animThread) {
      interrupted = false;
      do {
        if (restarted) {
          //System.err.println("run: notify");
          animThread.notify();
        }
        try {
          //System.err.println("run: wait");
          animThread.wait();
          //System.err.println("run: run");
        }
        catch (InterruptedException e) {
          break;
        }
        if (restarted)
          continue;
        boolean restart = false;
        animating = true;
        drawButtons = true;
        int[] mv = demo ? demoMove[0] : move[curMove];
        if (moveDir > 0) {
          if (movePos >= mv.length) {
            movePos = 0;
            initInfoText(mv);
          }
        }
        else {
          curInfoText = -1;
          if (movePos == 0)
            movePos = mv.length;
        }
        while (true) {
          if (moveDir < 0) {
            if (movePos == 0)
              break;
            movePos--;
          }
          if (mv[movePos] == -1) {
            repaint();
            if (!moveOne)
              sleep(33 * speed);
          }
          else if (mv[movePos] >= 1000) {
            curInfoText = moveDir > 0 ? mv[movePos] - 1000 : -1;
          }
          else {
            int num = mv[movePos] % 4 + 1;
            int mode = mv[movePos] / 4 % 6;
            boolean clockwise = num < 3;
            if (num == 4)
              num = 2;
            if (moveDir < 0) {
              clockwise = !clockwise;
              num = 4 - num;
            }
            spin(mv[movePos] / 24, num, mode, clockwise, moveAnimated);
            if (moveOne)
              restart = true;
          }
          if (moveDir > 0) {
            movePos++;
            if (movePos < mv.length && mv[movePos] >= 1000) {
              curInfoText = mv[movePos] - 1000;
              movePos++;
            }
            if (movePos == mv.length) {
              if (!demo)
                break;
              movePos = 0;
              initInfoText(mv);
              for (int i = 0; i < 6; i++)
                for (int j = 0; j < 9; j++)
                  cube[i][j] = initialCube[i][j];
            }
          }
          else
            curInfoText = -1;
          if (interrupted || restarted || restart)
            break;
        }
        animating = false;
        drawButtons = true;
        repaint();
        if (demo) {
          clear();
          demo = false;
        }
      } while (!interrupted);
    }
    //System.err.println("Interrupted!");
  } // run()

  private void sleep(int time) {
    synchronized (animThread) {
      try {
        animThread.wait(time);
      }
      catch (InterruptedException e) {
        interrupted = true;
      }
    }
  }

  private void clear() {
    synchronized (animThread) {
      movePos = 0;
      if (move.length > 0)
        initInfoText(move[curMove]);
      natural = true;
      mirrored = false;
      for (int i = 0; i < 6; i++)
        for (int j = 0; j < 9; j++)
          cube[i][j] = initialCube[i][j];
      for (int i = 0; i < 3; i++) {
        eye[i] = initialEye[i];
        eyeX[i] = initialEyeX[i];
        eyeY[i] = initialEyeY[i];
      }
    }
  }

  private void spin(int layer, int num, int mode, boolean clockwise, boolean animated) {
    twisting = false;
    natural = true;
    spinning = true;
    originalAngle = 0;
    if (faceTwistDirs[layer] > 0)
      clockwise = !clockwise;
    if (animated) {
      double phit = Math.PI / 2; // target for currentAngle (default pi/2)
      double phis = clockwise ? 1.0 : -1.0; // sign
      int turnTime = 67 * speed; // milliseconds to be used for one turn
      if (num == 2) {
        phit = Math.PI;
        turnTime = 67 * doubleSpeed; // double turn is usually faster than two quarter turns
      }
      twisting = true;
      twistedLayer = layer;
      twistedMode = mode;
      splitCube(layer); // start twisting
      long sTime = System.currentTimeMillis();
      long lTime = sTime;
      double d = phis * phit / turnTime;
      for (currentAngle = 0; currentAngle * phis < phit; currentAngle = d * (lTime - sTime)) {
        repaint();
        sleep(25);
        if (interrupted || restarted)
          break;
        lTime = System.currentTimeMillis();
      }
    }
    currentAngle = 0;
    twisting = false;
    natural = true;
    twistLayers(cube, layer, num, mode);
    spinning = false;
    if (animated)
      repaint();
  }

  // cube dimensions in number of facelets (mincol, maxcol, minrow, maxrow) for compact cube
  private static final int[][][] cubeBlocks = {
    {{0, 3}, {0, 3}}, // U
    {{0, 3}, {0, 3}}, // D
    {{0, 3}, {0, 3}}, // F
    {{0, 3}, {0, 3}}, // B
    {{0, 3}, {0, 3}}, // L
    {{0, 3}, {0, 3}}  // R
  };
  // subcube dimensions
  private final int[][][] topBlocks = new int[6][][];
  private final int[][][] midBlocks = new int[6][][];
  private final int[][][] botBlocks = new int[6][][];
  // all possible subcube dimensions for top and bottom layers
  private static final int[][][] topBlockTable = {
    {{0, 0}, {0, 0}},
    {{0, 3}, {0, 3}},
    {{0, 3}, {0, 1}},
    {{0, 1}, {0, 3}},
    {{0, 3}, {2, 3}},
    {{2, 3}, {0, 3}}
  };
  // subcube dimmensions for middle layers
  private static final int[][][] midBlockTable = {
    {{0, 0}, {0, 0}},
    {{0, 3}, {1, 2}},
    {{1, 2}, {0, 3}}
  };
  // indices to topBlockTable[] and botBlockTable[] for each twistedLayer value
  private static final int[][] topBlockFaceDim = {
  // U  D  F  B  L  R
    {1, 0, 3, 3, 2, 3}, // U
    {0, 1, 5, 5, 4, 5}, // D
    {2, 3, 1, 0, 3, 2}, // F
    {4, 5, 0, 1, 5, 4}, // B
    {3, 2, 2, 4, 1, 0}, // L
    {5, 4, 4, 2, 0, 1}  // R
  };
  private static final int[][] midBlockFaceDim = {
  // U  D  F  B  L  R
    {0, 0, 2, 2, 1, 2}, // U
    {0, 0, 2, 2, 1, 2}, // D
    {1, 2, 0, 0, 2, 1}, // F
    {1, 2, 0, 0, 2, 1}, // B
    {2, 1, 1, 1, 0, 0}, // L
    {2, 1, 1, 1, 0, 0}  // R
  };
  private static final int[][] botBlockFaceDim = {
  // U  D  F  B  L  R
    {0, 1, 5, 5, 4, 5}, // U
    {1, 0, 3, 3, 2, 3}, // D
    {4, 5, 0, 1, 5, 4}, // F
    {2, 3, 1, 0, 3, 2}, // B
    {5, 4, 4, 2, 0, 1}, // L
    {3, 2, 2, 4, 1, 0}  // R
  };

  private void splitCube(int layer) {
    for (int i = 0; i < 6; i++) { // for all faces
      topBlocks[i] = topBlockTable[topBlockFaceDim[layer][i]];
      botBlocks[i] = topBlockTable[botBlockFaceDim[layer][i]];
      midBlocks[i] = midBlockTable[midBlockFaceDim[layer][i]];
    }
    natural = false;
  }

  private void twistLayers(int[][] cube, int layer, int num, int mode) {
    switch (mode) {
     case 3:
      twistLayer(cube, layer ^ 1, num, false);
     case 2:
      twistLayer(cube, layer, 4 - num, false);
     case 1:
      twistLayer(cube, layer, 4 - num, true);
      break;
     case 5:
      twistLayer(cube, layer ^ 1, 4 - num, false);
      twistLayer(cube, layer, 4 - num, false);
      break;
     case 4:
      twistLayer(cube, layer ^ 1, num, false);
     default:
      twistLayer(cube, layer, 4 - num, false);
    }
  }

  // top facelet cycle
  private static final int[] cycleOrder = {0, 1, 2, 5, 8, 7, 6, 3};
  // side facelet cycle offsets
  private static final int[] cycleFactors = {1, 3, -1, -3, 1, 3, -1, -3};
  private static final int[] cycleOffsets = {0, 2,  8,  6, 3, 1,  5,  7};
  // indices for faces of layers
  private static final int[][] cycleLayerSides = {
    {3, 3, 3, 0}, // U: F=6-3k R=6-3k B=6-3k L=k
    {2, 1, 1, 1}, // D: L=8-k  B=2+3k R=2+3k F=2+3k
    {3, 3, 0, 0}, // F: L=6-3k D=6-3k R=k    U=k
    {2, 1, 1, 2}, // B: R=8-k  D=2+3k L=2+3k U=8-k
    {3, 2, 0, 0}, // L: U=6-3k B=8-k  D=k    F=k
    {2, 2, 0, 1}  // R: F=8-k  D=8-k  B=k    U=2+3k
  };
  // indices for sides of center layers
  private static final int[][] cycleCenters = {
    {7, 7, 7, 4}, // E'(U): F=7-3k R=7-3k B=7-3k L=3+k
    {6, 5, 5, 5}, // E (D): L=5-k  B=1+3k R=1+3k F=1+3k
    {7, 7, 4, 4}, // S (F): L=7-3k D=7-3k R=3+k  U=3+k
    {6, 5, 5, 6}, // S'(B): R=5-k  D=1+3k L=1+3k U=5-k
    {7, 6, 4, 4}, // M (L): U=7-3k B=8-k  D=3+k  F=3+k
    {6, 6, 4, 5}  // M'(R): F=5-k  D=5-k  B=3+k  U=1+3k
  };
  private final int[] twistBuffer = new int[12];

  private void twistLayer(int[][] cube, int layer, int num, boolean middle) {
    if (!middle) {
      // rotate top facelets
      for (int i = 0; i < 8; i++) // to buffer
        twistBuffer[(i + num * 2) % 8] = cube[layer][cycleOrder[i]];
      for (int i = 0; i < 8; i++) // to cube
        cube[layer][cycleOrder[i]] = twistBuffer[i];
    }
    // rotate side facelets
    int k = num * 3;
    for (int i = 0; i < 4; i++) { // to buffer
      int n = adjacentFaces[layer][i];
      int c = middle ? cycleCenters[layer][i] : cycleLayerSides[layer][i];
      int factor = cycleFactors[c];
      int offset = cycleOffsets[c];
      for (int j = 0; j < 3; j++) {
        twistBuffer[k % 12] = cube[n][j * factor + offset];
        k++;
      }
    }
    k = 0; // MS VM JIT bug if placed into the loop init
    for (int i = 0; i < 4; i++) { // to cube
      int n = adjacentFaces[layer][i];
      int c = middle ? cycleCenters[layer][i] : cycleLayerSides[layer][i];
      int factor = cycleFactors[c];
      int offset = cycleOffsets[c];
      int j = 0; // MS VM JIT bug if for is used
      while (j < 3) {
        cube[n][j * factor + offset] = twistBuffer[k];
        j++;
        k++;
      }
    }
  }

  // double buffered animation
  private Graphics graphics = null;
  private Image image = null;
  // cube window size (applet window is resizable)
  private int width;
  private int height;
  // last position of mouse (for dragging the cube)
  private int lastX;
  private int lastY;
  // last position of mouse (when waiting for clear decission)
  private int lastDragX;
  private int lastDragY;
  // drag areas
  private int dragAreas;
  private final int[][] dragCornersX = new int[18][4];
  private final int[][] dragCornersY = new int[18][4];
  private final double[] dragDirsX = new double[18];
  private final double[] dragDirsY = new double[18];
  private static final int[][][] dragBlocks = {
    {{0, 0}, {3, 0}, {3, 1}, {0, 1}},
    {{3, 0}, {3, 3}, {2, 3}, {2, 0}},
    {{3, 3}, {0, 3}, {0, 2}, {3, 2}},
    {{0, 3}, {0, 0}, {1, 0}, {1, 3}},
    // center slices
    {{0, 1}, {3, 1}, {3, 2}, {0, 2}},
    {{2, 0}, {2, 3}, {1, 3}, {1, 0}}
  };
  private static final int[][] areaDirs = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}, {1, 0}, {0, 1}};
  private static final int[][] twistDirs = {
    { 1,  1,  1,  1,  1, -1}, // U
    { 1,  1,  1,  1,  1, -1}, // D
    { 1, -1,  1, -1,  1,  1}, // F
    { 1, -1,  1, -1, -1,  1}, // B
    {-1,  1, -1,  1, -1, -1}, // L
    { 1, -1,  1, -1,  1,  1}  // R
  };
  private int[] dragLayers = new int[18]; // which layers belongs to dragCorners
  private int[] dragModes = new int[18]; // which layer modes dragCorners
  // current drag directions
  private double dragX;
  private double dragY;
  // various sign tables for computation of directions of rotations
  private static final int[][][] rotCos = {
    {{ 1,  0,  0}, { 0,  0,  0}, { 0,  0,  1}}, // U-D
    {{ 1,  0,  0}, { 0,  1,  0}, { 0,  0,  0}}, // F-B
    {{ 0,  0,  0}, { 0,  1,  0}, { 0,  0,  1}}  // L-R
  };
  private static final int[][][] rotSin = {
    {{ 0,  0,  1}, { 0,  0,  0}, {-1,  0,  0}}, // U-D
    {{ 0,  1,  0}, {-1,  0,  0}, { 0,  0,  0}}, // F-B
    {{ 0,  0,  0}, { 0,  0,  1}, { 0, -1,  0}}  // L-R
  };
  private static final int[][][] rotVec = {
    {{ 0,  0,  0}, { 0,  1,  0}, { 0,  0,  0}}, // U-D
    {{ 0,  0,  0}, { 0,  0,  0}, { 0,  0,  1}}, // F-B
    {{ 1,  0,  0}, { 0,  0,  0}, { 0,  0,  0}}  // L-R
  };
  private static final int[] rotSign = {1, -1, 1, -1, 1, -1}; // U, D, F, B, L, R
  // temporary eye vectors for twisted sub-cube rotation
  private final double[] tempEye = new double[3];
  private final double[] tempEyeX = new double[3];
  private final double[] tempEyeY = new double[3];
  // temporary eye vectors for second twisted sub-cube rotation (antislice)
  private final double[] tempEye2 = new double[3];
  private final double[] tempEyeX2 = new double[3];
  private final double[] tempEyeY2 = new double[3];
  // temporary vectors to compute visibility in perspective projection
  private final double[] perspEye = new double[3];
  private final double[] perspEyeI = new double[3];
  private final double[] perspNormal = new double[3];
  // eye arrays to store various eyes for various modes
  private final double[][] eyeArray = new double[3][];
  private final double[][] eyeArrayX = new double[3][];
  private final double[][] eyeArrayY = new double[3][];
  private final int[][] eyeOrder = {{1, 0, 0}, {0, 1, 0}, {1, 1, 0}, {1, 1, 1}, {1, 0, 1}, {1, 0, 2}};
  private final int[][][][] blockArray = new int[3][][][];
  private final int[][] blockMode = {{0, 2, 2}, {2, 1, 2}, {2, 2, 2}, {2, 2, 2}, {2, 2, 2}, {2, 2, 2}};
  private final int[][] drawOrder = {{0, 1, 2}, {2, 1, 0}, {0, 2, 1}};

  public void paint(Graphics g) {
    Dimension size = getSize(); // inefficient - Java 1.1
    // create offscreen buffer for double buffering
    if (image == null || size.width != width || size.height - buttonHeight != height) {
      width = size.width;
      height = size.height;
      image = createImage(width, height);
      graphics = image.getGraphics();
      textHeight = graphics.getFontMetrics().getHeight() - graphics.getFontMetrics().getLeading();
      if (buttonBar == 1)
        height -= buttonHeight;
      drawButtons = true;
    }
    graphics.setColor(bgColor);
    graphics.setClip(0, 0, width, height);
    graphics.fillRect(0, 0, width, height);
    synchronized (animThread) {
      dragAreas = 0;
      if (natural) // compact cube
        fixBlock(eye, eyeX, eyeY, cubeBlocks, 3); // draw cube and fill drag areas
      else { // in twisted state
        // compute top observer
        double cosA = Math.cos(originalAngle + currentAngle);
        double sinA = Math.sin(originalAngle + currentAngle) * rotSign[twistedLayer];
        for (int i = 0; i < 3; i++) {
          tempEye[i] = 0;
          tempEyeX[i] = 0;
          for (int j = 0; j < 3; j++) {
            int axis = twistedLayer / 2;
            tempEye[i] += eye[j] * (rotVec[axis][i][j] + rotCos[axis][i][j] * cosA + rotSin[axis][i][j] * sinA);
            tempEyeX[i] += eyeX[j] * (rotVec[axis][i][j] + rotCos[axis][i][j] * cosA + rotSin[axis][i][j] * sinA);
          }
        }
        vMul(tempEyeY, tempEye, tempEyeX);
        // compute bottom anti-observer
        double cosB = Math.cos(originalAngle - currentAngle);
        double sinB = Math.sin(originalAngle - currentAngle) * rotSign[twistedLayer];
        for (int i = 0; i < 3; i++) {
          tempEye2[i] = 0;
          tempEyeX2[i] = 0;
          for (int j = 0; j < 3; j++) {
            int axis = twistedLayer / 2;
            tempEye2[i] += eye[j] * (rotVec[axis][i][j] + rotCos[axis][i][j] * cosB + rotSin[axis][i][j] * sinB);
            tempEyeX2[i] += eyeX[j] * (rotVec[axis][i][j] + rotCos[axis][i][j] * cosB + rotSin[axis][i][j] * sinB);
          }
        }
        vMul(tempEyeY2, tempEye2, tempEyeX2);
        eyeArray[0] = eye;
        eyeArrayX[0] = eyeX;
        eyeArrayY[0] = eyeY;
        eyeArray[1] = tempEye;
        eyeArrayX[1] = tempEyeX;
        eyeArrayY[1] = tempEyeY;
        eyeArray[2] = tempEye2;
        eyeArrayX[2] = tempEyeX2;
        eyeArrayY[2] = tempEyeY2;
        blockArray[0] = topBlocks;
        blockArray[1] = midBlocks;
        blockArray[2] = botBlocks;
        // perspective corrections
        vSub(vScale(vCopy(perspEye, eye), 5.0 + persp), vScale(vCopy(perspNormal, faceNormals[twistedLayer]), 1.0 / 3.0));
        vSub(vScale(vCopy(perspEyeI, eye), 5.0 + persp), vScale(vCopy(perspNormal, faceNormals[twistedLayer ^ 1]), 1.0 / 3.0));
        double topProd = vProd(perspEye, faceNormals[twistedLayer]);
        double botProd = vProd(perspEyeI, faceNormals[twistedLayer ^ 1]);
        int orderMode;
        if (topProd < 0 && botProd > 0) // top facing away
          orderMode = 0;
        else if (topProd > 0 && botProd < 0) // bottom facing away: draw it first
          orderMode = 1;
        else // both top and bottom layer facing away: draw them first
          orderMode = 2;
        fixBlock(eyeArray[eyeOrder[twistedMode][drawOrder[orderMode][0]]],
                 eyeArrayX[eyeOrder[twistedMode][drawOrder[orderMode][0]]],
                 eyeArrayY[eyeOrder[twistedMode][drawOrder[orderMode][0]]],
                 blockArray[drawOrder[orderMode][0]],
                 blockMode[twistedMode][drawOrder[orderMode][0]]);
        fixBlock(eyeArray[eyeOrder[twistedMode][drawOrder[orderMode][1]]],
                 eyeArrayX[eyeOrder[twistedMode][drawOrder[orderMode][1]]],
                 eyeArrayY[eyeOrder[twistedMode][drawOrder[orderMode][1]]],
                 blockArray[drawOrder[orderMode][1]],
                 blockMode[twistedMode][drawOrder[orderMode][1]]);
        fixBlock(eyeArray[eyeOrder[twistedMode][drawOrder[orderMode][2]]],
                 eyeArrayX[eyeOrder[twistedMode][drawOrder[orderMode][2]]],
                 eyeArrayY[eyeOrder[twistedMode][drawOrder[orderMode][2]]],
                 blockArray[drawOrder[orderMode][2]],
                 blockMode[twistedMode][drawOrder[orderMode][2]]);
      }
      if (!pushed && !animating) // no button should be deceased
        buttonPressed = -1;
      if (!demo && move.length > 0) {
        if (move[curMove].length > 0) { // some turns
          graphics.setColor(Color.black);
          graphics.drawRect(0, height - progressHeight, width - 1, progressHeight - 1);
          graphics.setColor(textColor);
          int progress = (width - 2) * realMovePos(move[curMove], movePos) / realMoveLength(move[curMove]);
          graphics.fillRect(1, height - progressHeight + 1, progress, progressHeight - 2);
          graphics.setColor(bgColor.darker());
          graphics.fillRect(1 + progress, height - progressHeight + 1, width - 2 - progress, progressHeight - 2);
          String s = "" + moveLength(move[curMove], movePos) + "/" + moveLength(move[curMove], -1) + metricChar[metric];
          int w = graphics.getFontMetrics().stringWidth(s);
          int x = width - w - 2;
          int y = height - progressHeight - 2; //int base = graphics.getFontMetrics().getDescent();
          if (moveText > 0 && textHeight > 0) {
            drawString(graphics, s, x, y - textHeight);
            drawMoveText(graphics, y);
          }
          else
            drawString(graphics, s, x, y);
        }
        if (move.length > 1) { // more sequences
          graphics.setClip(0, 0, width, height);
          int b = graphics.getFontMetrics().getDescent();
          int y = textHeight - b;
          String s = "" + (curMove + 1) + "/" + move.length;
          int w = graphics.getFontMetrics().stringWidth(s);
          int x = width - w - buttonHeight - 2;
          drawString(graphics, s, x, y);
          // draw button
          graphics.setColor(buttonBgColor);
          graphics.fill3DRect(width - buttonHeight, 0, buttonHeight, buttonHeight, buttonPressed != 7);
          drawButton(graphics, 7, width - buttonHeight / 2, buttonHeight / 2);
        }
      }
      if (curInfoText >= 0) {
        graphics.setClip(0, 0, width, height);
        int b = graphics.getFontMetrics().getDescent();
        int y = textHeight - b;
        drawString(graphics, infoText[curInfoText], 0, y);
      }
      if (drawButtons && buttonBar != 0) // omit unneccessary redrawing
        drawButtons(graphics);
    }
    g.drawImage(image, 0, 0, this);
  } // paint()

  public void update(Graphics g) {
    paint(g);
  }

  // polygon co-ordinates to fill (cube faces or facelets)
  private final int[] fillX = new int[4];
  private final int[] fillY = new int[4];
  // projected vertex co-ordinates (to screen)
  private final double[] coordsX = new double[8];
  private final double[] coordsY = new double[8];
  private final double[][] cooX = new double[6][4];
  private final double[][] cooY = new double[6][4];
  private static final double[][] border = {{0.10, 0.10}, {0.90, 0.10}, {0.90, 0.90}, {0.10, 0.90}};
  private static final int[][] factors = {{0, 0}, {0, 1}, {1, 1}, {1, 0}};
  private final double[] faceShiftX = new double[6];
  private final double[] faceShiftY = new double[6];
  private final double[] tempNormal = new double[3];

  private void fixBlock(double[] eye, double[] eyeX, double[] eyeY, int[][][] blocks, int mode) {
    // project 3D co-ordinates into 2D screen ones
    for (int i = 0; i < 8; i++) {
      double min = width < height ? width : height - progressHeight;
      double x = min / 3.7 * vProd(cornerCoords[i], eyeX) * scale;
      double y = min / 3.7 * vProd(cornerCoords[i], eyeY) * scale;
      double z = min / (5.0 + persp) * vProd(cornerCoords[i], eye) * scale;
      x = x / (1 - z / min); // perspective transformation
      y = y / (1 - z / min); // perspective transformation
      coordsX[i] = width / 2.0 + x;
      if (align == 0)
        coordsY[i] = (height - progressHeight) / 2.0 * scale  - y;
      else if (align == 2)
        coordsY[i] = height - progressHeight - (height - progressHeight) / 2.0 * scale  - y;
      else
        coordsY[i] = (height - progressHeight) / 2.0 - y;
    }
    // setup corner co-ordinates for all faces
    for (int i = 0; i < 6; i++) { // all faces
      for (int j = 0; j < 4; j++) { // all face corners
        cooX[i][j] = coordsX[faceCorners[i][j]];
        cooY[i][j] = coordsY[faceCorners[i][j]];
      }
    }
    if (hint) { // draw hint hiden facelets
      for (int i = 0; i < 6; i++) { // all faces
        vSub(vScale(vCopy(perspEye, eye), 5.0 + persp), faceNormals[i]); // perspective correction
        if (vProd(perspEye, faceNormals[i]) < 0) { // draw only hiden faces
          vScale(vCopy(tempNormal, faceNormals[i]), faceShift);
          double min = width < height ? width : height - progressHeight;
          double x = min / 3.7 * vProd(tempNormal, eyeX);
          double y = min / 3.7 * vProd(tempNormal, eyeY);
          double z = min / (5.0 + persp) * vProd(tempNormal, eye);
          x = x / (1 - z / min); // perspective transformation
          y = y / (1 - z / min); // perspective transformation
          int sideW = blocks[i][0][1] - blocks[i][0][0];
          int sideH = blocks[i][1][1] - blocks[i][1][0];
          if (sideW > 0 && sideH > 0) { // this side is not only black
            // draw colored facelets
            for (int n = 0, p = blocks[i][1][0]; n < sideH; n++, p++) {
              for (int o = 0, q = blocks[i][0][0]; o < sideW; o++, q++) {
                for (int j = 0; j < 4; j++) {
                  getCorners(i, j, fillX, fillY, q + border[j][0], p + border[j][1], mirrored);
                  fillX[j] += mirrored ? -x : x;
                  fillY[j] -= y;
                }
                graphics.setColor(colors[cube[i][p * 3 + q]]);
                graphics.fillPolygon(fillX, fillY, 4);
                graphics.setColor(colors[cube[i][p * 3 + q]].darker());
                graphics.drawPolygon(fillX, fillY, 4);
              }
            }
          }
        }
      }
    }
    // draw black antialias
    for (int i = 0; i < 6; i++) { // all faces
      int sideW = blocks[i][0][1] - blocks[i][0][0];
      int sideH = blocks[i][1][1] - blocks[i][1][0];
      if (sideW > 0 && sideH > 0) {
        for (int j = 0; j < 4; j++) // corner co-ordinates
          getCorners(i, j, fillX, fillY, blocks[i][0][factors[j][0]], blocks[i][1][factors[j][1]], mirrored);
        if (sideW == 3 && sideH == 3)
          graphics.setColor(bgColor2);
        else
          graphics.setColor(Color.black);
        graphics.drawPolygon(fillX, fillY, 4);
      }
    }
    // find and draw black inner faces
    for (int i = 0; i < 6; i++) { // all faces
      int sideW = blocks[i][0][1] - blocks[i][0][0];
      int sideH = blocks[i][1][1] - blocks[i][1][0];
      if (sideW <= 0 || sideH <= 0) { // this face is inner and only black
        for (int j = 0; j < 4; j++) { // for all corners
          int k = oppositeCorners[i][j];
          fillX[j] = (int)(cooX[i][j] + (cooX[i ^ 1][k] - cooX[i][j]) * 2.0 / 3.0);
          fillY[j] = (int)(cooY[i][j] + (cooY[i ^ 1][k] - cooY[i][j]) * 2.0 / 3.0);
          if (mirrored)
            fillX[j] = width - fillX[j];
        }
        graphics.setColor(Color.black);
        graphics.fillPolygon(fillX, fillY, 4);
      }
      else {
        // draw black face background (do not care about normals and visibility!)
        for (int j = 0; j < 4; j++) // corner co-ordinates
          getCorners(i, j, fillX, fillY, blocks[i][0][factors[j][0]], blocks[i][1][factors[j][1]], mirrored);
        graphics.setColor(Color.black);
        graphics.fillPolygon(fillX, fillY, 4);
      }
    }
    // draw all visible faces and get dragging regions
    for (int i = 0; i < 6; i++) { // all faces
      vSub(vScale(vCopy(perspEye, eye), 5.0 + persp), faceNormals[i]); // perspective correction
      if (vProd(perspEye, faceNormals[i]) > 0) { // draw only faces towards us
        int sideW = blocks[i][0][1] - blocks[i][0][0];
        int sideH = blocks[i][1][1] - blocks[i][1][0];
        if (sideW > 0 && sideH > 0) { // this side is not only black
          // draw colored facelets
          for (int n = 0, p = blocks[i][1][0]; n < sideH; n++, p++) {
            for (int o = 0, q = blocks[i][0][0]; o < sideW; o++, q++) {
              for (int j = 0; j < 4; j++)
                getCorners(i, j, fillX, fillY, q + border[j][0], p + border[j][1], mirrored);
              graphics.setColor(colors[cube[i][p * 3 + q]].darker());
              graphics.drawPolygon(fillX, fillY, 4);
              graphics.setColor(colors[cube[i][p * 3 + q]]);
              graphics.fillPolygon(fillX, fillY, 4);
            }
          }
        }
        if (!editable || animating) // no need of twisting while animating
          continue;
        // horizontal and vertical directions of face - interpolated
        double dxh = (cooX[i][1] - cooX[i][0] + cooX[i][2] - cooX[i][3]) / 6.0;
        double dyh = (cooX[i][3] - cooX[i][0] + cooX[i][2] - cooX[i][1]) / 6.0;
        double dxv = (cooY[i][1] - cooY[i][0] + cooY[i][2] - cooY[i][3]) / 6.0;
        double dyv = (cooY[i][3] - cooY[i][0] + cooY[i][2] - cooY[i][1]) / 6.0;
        if (mode == 3) { // just the normal cube
          for (int j = 0; j < 6; j++) { // 4 areas 3x1 per face + 2 center slices
            for (int k = 0; k < 4; k++) // 4 points per area
              getCorners(i, k, dragCornersX[dragAreas], dragCornersY[dragAreas],
                               dragBlocks[j][k][0], dragBlocks[j][k][1], false);
            dragDirsX[dragAreas] = (dxh * areaDirs[j][0] + dxv * areaDirs[j][1]) * twistDirs[i][j];
            dragDirsY[dragAreas] = (dyh * areaDirs[j][0] + dyv * areaDirs[j][1]) * twistDirs[i][j];
            dragLayers[dragAreas] = adjacentFaces[i][j % 4];
            if (j >= 4)
              dragLayers[dragAreas] &= ~1;
            dragModes[dragAreas] = j / 4;
            dragAreas++;
            if (dragAreas == 18)
              break;
          }
        }
        else if (mode == 0) { // twistable top layer
          if (i != twistedLayer && sideW > 0 && sideH > 0) { // only 3x1 faces
            int j = sideW == 3 ? (blocks[i][1][0] == 0 ? 0 : 2) : (blocks[i][0][0] == 0 ? 3 : 1);
            for (int k = 0; k < 4; k++)
              getCorners(i, k, dragCornersX[dragAreas], dragCornersY[dragAreas],
                               dragBlocks[j][k][0], dragBlocks[j][k][1], false);
            dragDirsX[dragAreas] = (dxh * areaDirs[j][0] + dxv * areaDirs[j][1]) * twistDirs[i][j];
            dragDirsY[dragAreas] = (dyh * areaDirs[j][0] + dyv * areaDirs[j][1]) * twistDirs[i][j];
            dragLayers[dragAreas] = twistedLayer;
            dragModes[dragAreas] = 0;
            dragAreas++;
          }
        }
        else if (mode == 1) { // twistable center layer
          if (i != twistedLayer && sideW > 0 && sideH > 0) { // only 3x1 faces
            int j = sideW == 3 ? 4 : 5;
            for (int k = 0; k < 4; k++)
              getCorners(i, k, dragCornersX[dragAreas], dragCornersY[dragAreas],
                               dragBlocks[j][k][0], dragBlocks[j][k][1], false);
            dragDirsX[dragAreas] = (dxh * areaDirs[j][0] + dxv * areaDirs[j][1]) * twistDirs[i][j];
            dragDirsY[dragAreas] = (dyh * areaDirs[j][0] + dyv * areaDirs[j][1]) * twistDirs[i][j];
            dragLayers[dragAreas] = twistedLayer;
            dragModes[dragAreas] = 1;
            dragAreas++;
          }
        }
      }
    }
  }

  private void getCorners(int face, int corner, int[] cornersX, int[] cornersY, double factor1, double factor2, boolean mirror) {
    factor1 /= 3.0;
    factor2 /= 3.0;
    double x1 = cooX[face][0] + (cooX[face][1] - cooX[face][0]) * factor1;
    double y1 = cooY[face][0] + (cooY[face][1] - cooY[face][0]) * factor1;
    double x2 = cooX[face][3] + (cooX[face][2] - cooX[face][3]) * factor1;
    double y2 = cooY[face][3] + (cooY[face][2] - cooY[face][3]) * factor1;
    cornersX[corner] = (int)(0.5 + x1 + (x2 - x1) * factor2);
    cornersY[corner] = (int)(0.5 + y1 + (y2 - y1) * factor2);
    if (mirror)
      cornersX[corner] = width - cornersX[corner];
  }

  private void drawButtons(Graphics g) {
    if (buttonBar == 2) { // only clear (rewind) button
      g.setColor(buttonBgColor);
      g.fill3DRect(0, height - buttonHeight, buttonHeight, buttonHeight, buttonPressed != 0);
      drawButton(g, 0, buttonHeight / 2, height - (buttonHeight + 1) / 2);
      return;
    }
    if (buttonBar == 1) { // full buttonbar
      g.setClip(0, height, width, buttonHeight);
      int buttonX = 0;
      for (int i = 0; i < 7; i++) {
        int buttonWidth = (width - buttonX) / (7 - i);
        g.setColor(buttonBgColor);
        g.fill3DRect(buttonX, height, buttonWidth, buttonHeight, buttonPressed != i);
        drawButton(g, i, buttonX + buttonWidth / 2, height + buttonHeight / 2);
        buttonX += buttonWidth;
      }
      drawButtons = false;
      return;
    }
  }

  private void drawButton(Graphics g, int i, int x, int y) {
    g.setColor(Color.white);
    switch (i) {
     case 0: // rewind
      drawRect(g, x - 4, y - 3, 3, 7);
      drawArrow(g, x + 3, y, -1); // left
      break;
     case 1: // reverse step
      drawRect(g, x + 2, y - 3, 3, 7);
      drawArrow(g, x, y, -1); // left
      break;
     case 2: // reverse play
      drawArrow(g, x + 2, y, -1); // left
      break;
     case 3: // stop / mirror
      if (animating)
        drawRect(g, x - 3, y - 3, 7, 7);
      else {
        drawRect(g, x - 3, y - 2, 7, 5);
        drawRect(g, x - 1, y - 4, 3, 9);
      }
      break;
     case 4: // play
      drawArrow(g, x - 2, y, 1); // right
      break;
     case 5: // step
      drawRect(g, x - 4, y - 3, 3, 7);
      drawArrow(g, x, y, 1); // right
      break;
     case 6: // fast forward
      drawRect(g, x + 1, y - 3, 3, 7);
      drawArrow(g, x - 4, y, 1); // right
      break;
     case 7: // next sequence
      drawArrow(g, x - 2, y, 1); // right
      break;
    }
  }

  private static void drawArrow(Graphics g, int x, int y, int dir) {
    g.setColor(Color.black);
    g.drawLine(x, y - 3, x, y + 3);
    x += dir;
    for (int i = 0; i >= -3 && i <= 3; i += dir) {
      int j = 3 - i * dir;
      g.drawLine(x + i, y + j, x + i, y - j);
    }
    g.setColor(Color.white);
    for (int i = 0; i >= -1 && i <= 1; i += dir) {
      int j = 1 - i * dir;
      g.drawLine(x + i, y + j, x + i, y - j);
    }
  }

  private static void drawRect(Graphics g, int x, int y, int width, int height) {
    g.setColor(Color.black);
    g.drawRect(x, y, width - 1, height - 1);
    g.setColor(Color.white);
    g.fillRect(x + 1, y + 1, width - 2, height - 2);
  }

  private static final int[] textOffset = {1, 1, -1, -1, -1, 1, 1, -1, -1, 0, 1, 0, 0, 1, 0, -1};

  private void drawString(Graphics g, String s, int x, int y) {
    if (outlined) {
      g.setColor(Color.black);
      for (int i = 0; i < textOffset.length; i += 2)
        g.drawString(s, x + textOffset[i], y + textOffset[i + 1]);
      g.setColor(Color.white);
    }
    else
      g.setColor(textColor);
    g.drawString(s, x, y);
  }

  private void drawMoveText(Graphics g, int y) {
    g.setClip(0, height - progressHeight - textHeight, width, textHeight);
    g.setColor(Color.black);
    int pos = movePos == 0 ? arrayMovePos(move[curMove], movePos) : movePos;
    String s1 = moveText(move[curMove], 0, pos);
    String s2 = turnText(move[curMove], pos);
    String s3 = moveText(move[curMove], pos + 1, move[curMove].length);
    int w1 = g.getFontMetrics().stringWidth(s1);
    int w2 = g.getFontMetrics().stringWidth(s2);
    int w3 = g.getFontMetrics().stringWidth(s3);
    int x = 1;
    if (x + w1 + w2 + w3 > width) {
      x = Math.min(1, width / 2 - w1 - w2 / 2);
      x = Math.max(x, width - w1 - w2 - w3 - 2);
    }
    if (w2 > 0) {
      g.setColor(hlColor);
      g.fillRect(x + w1 - 1, height - progressHeight - textHeight, w2 + 2, textHeight);
    }
    if (w1 > 0)
      drawString(g, s1, x, y);
    if (w2 > 0)
      drawString(g, s2, x + w1, y);
    if (w3 > 0)
      drawString(g, s3, x + w1 + w2, y);
  }

  private int selectButton(int x, int y) {
    if (buttonBar == 0)
      return -1;
    if (move.length > 1 && x >= width - buttonHeight && x < width && y >= 0 && y < buttonHeight)
      return 7;
    if (buttonBar == 2) { // only clear (rewind) button present
      if (x >= 0 && x < buttonHeight && y >= height - buttonHeight && y < height)
        return 0;
      return -1;
    }
    if (y < height)
      return -1;
    int buttonX = 0;
    for (int i = 0; i < 7; i++) {
      int buttonWidth = (width - buttonX) / (7 - i);
      if (x >= buttonX && x < buttonX + buttonWidth && y >= height && y < height + buttonHeight)
        return i;
      buttonX += buttonWidth;
    }
    return -1;
  }

  // Mouse event handlers

  private final static int[] buttonAction = {-1, 3, 1, -1, 0, 2, 4, -1};

  public void mousePressed(MouseEvent e) {
    lastDragX = lastX = e.getX();
    lastDragY = lastY = e.getY();
    toTwist = false;
    buttonPressed = selectButton(lastX, lastY);
    if (buttonPressed >= 0) {
      pushed = true;
      if (buttonPressed == 3) {
        if (!animating) // special feature
          mirrored = !mirrored;
        else
          stopAnimation();
      }
      else if (buttonPressed == 0) { // clear everything to the initial setup
        stopAnimation();
        clear();
      }
      else if (buttonPressed == 7) { // next sequence
        stopAnimation();
        clear();
        curMove = curMove < move.length - 1 ? curMove + 1 : 0;
      }
      else
        startAnimation(buttonAction[buttonPressed]);
      drawButtons = true;
      repaint();
    }
    else if (progressHeight > 0 && move.length > 0 && move[curMove].length > 0 && lastY >= height - progressHeight && lastY < height) {
      stopAnimation();
      int len = realMoveLength(move[curMove]);
      int pos = ((lastX - 1) * len * 2 / (width - 2) + 1) / 2;
      pos = Math.max(0, Math.min(len, pos));
      if (pos > 0)
        pos = arrayMovePos(move[curMove], pos);
      if (pos > movePos)
        doMove(cube, move[curMove], movePos, pos - movePos, false);
      if (pos < movePos)
        doMove(cube, move[curMove], pos, movePos - pos, true);
      movePos = pos;
      dragging = true;
      repaint();
    }
    else {
      if (mirrored)
        lastDragX = lastX = width - lastX;
      if (editable && !animating &&
       (e.getModifiers() & InputEvent.BUTTON1_MASK) != 0 &&
       (e.getModifiers() & InputEvent.SHIFT_MASK) == 0)
        toTwist = true;
    }
  }

  public void mouseReleased(MouseEvent e) {
    dragging = false;
    if (pushed) {
      pushed = false;
      drawButtons = true;
      repaint();
    }
    else if (twisting && !spinning) {
      twisting = false;
      originalAngle += currentAngle;
      currentAngle = 0.0;
      double angle = originalAngle;
      while (angle < 0.0)
        angle += 32.0 * Math.PI;
      int num = (int)(angle * 8.0 / Math.PI) % 16; // 2pi ~ 16
      if (num % 4 == 0 || num % 4 == 3) { // close enough to a corner
        num = (num + 1) / 4; // 2pi ~ 4
        if (faceTwistDirs[twistedLayer] > 0)
          num = (4 - num) % 4;
        originalAngle = 0;
        natural = true; // the cube in the natural state
        twistLayers(cube, twistedLayer, num, twistedMode); // rotate the facelets
      }
      repaint();
    }
  }

  private final double[] eyeD = new double[3];

  public void mouseDragged(MouseEvent e) {
    if (pushed)
      return;
    if (dragging) {
      stopAnimation();
      int len = realMoveLength(move[curMove]);
      int pos = ((e.getX() - 1) * len * 2 / (width - 2) + 1) / 2;
      pos = Math.max(0, Math.min(len, pos));
      if (pos > 0)
        pos = arrayMovePos(move[curMove], pos);
      if (pos > movePos)
        doMove(cube, move[curMove], movePos, pos - movePos, false);
      if (pos < movePos)
        doMove(cube, move[curMove], pos, movePos - pos, true);
      movePos = pos;
      repaint();
      return;
    }
    int x = mirrored ? width - e.getX() : e.getX();
    int y = e.getY();
    int dx = x - lastX;
    int dy = y - lastY;
    if (editable && toTwist && !twisting && !animating) { // we do not twist but we can
      lastDragX = x;
      lastDragY = y;
      for (int i = 0; i < dragAreas; i++) { // check if inside a drag area
        double d1 = dragCornersX[i][0];
        double x1 = dragCornersX[i][1] - d1;
        double y1 = dragCornersX[i][3] - d1;
        double d2 = dragCornersY[i][0];
        double x2 = dragCornersY[i][1] - d2;
        double y2 = dragCornersY[i][3] - d2;
        double a = (y2 * (lastX - d1) - y1 * (lastY - d2)) / (x1 * y2 - y1 * x2);
        double b = (-x2 * (lastX - d1) + x1 * (lastY - d2)) / (x1 * y2 - y1 * x2);
        if (a > 0 && a < 1 && b > 0 && b < 1) { // we are in
          if (dx * dx + dy * dy < 144) // delay the decision about twisting
            return;
          dragX = dragDirsX[i];
          dragY = dragDirsY[i];
          double d = Math.abs(dragX * dx + dragY * dy) / Math.sqrt((dragX * dragX + dragY * dragY) * (dx * dx + dy * dy));
          if (d > 0.75) {
            twisting = true;
            twistedLayer = dragLayers[i];
            twistedMode = dragModes[i];
            break;
          }
        }
      }
      toTwist = false;
      lastX = lastDragX;
      lastY = lastDragY;
    }
    dx = x - lastX;
    dy = y - lastY;
    if (!twisting || animating) { // whole cube rotation
      vNorm(vAdd(eye, vScale(vCopy(eyeD, eyeX), dx * -0.016)));
      vNorm(vMul(eyeX, eyeY, eye));
      vNorm(vAdd(eye, vScale(vCopy(eyeD, eyeY), dy * 0.016)));
      vNorm(vMul(eyeY, eye, eyeX));
      lastX = x;
      lastY = y;
    }
    else {
      if (natural)
        splitCube(twistedLayer);
      currentAngle = 0.03 * (dragX * dx + dragY * dy) / Math.sqrt(dragX * dragX + dragY * dragY); // dv * cos a
    }
    repaint();
  }

  // status bar help strings
  private static final String[] buttonDescriptions = {
    "Clear to the initial state",
    "Show the previous step",
    "Play backward",
    "Stop",
    "Play",
    "Show the next step",
    "Go to the end",
    "Next sequence"
  };
  private String buttonDescription = "";

  public void mouseMoved(MouseEvent e) {
    int x = e.getX();
    int y = e.getY();
    String description = "Drag the cube with a mouse";
    if (x >= 0 && x < width) {
      if (y >= height && y < height + buttonHeight || y >= 0 && y < buttonHeight) {
        buttonPressed = selectButton(x, y);
        if (buttonPressed >= 0)
          description = buttonDescriptions[buttonPressed];
        if (buttonPressed == 3 && !animating)
          description = "Mirror the cube view";
      }
      else if (progressHeight > 0 && move.length > 0 && move[curMove].length > 0 && y >= height - progressHeight && y < height) {
        description = "Current progress";
      }
    }
    if (description != buttonDescription) {
      buttonDescription = description;
      showStatus(description);
    }
  }

  public void mouseClicked(MouseEvent e) {}
  public void mouseEntered(MouseEvent e) {}
  public void mouseExited(MouseEvent e) {}

  // Various useful vector functions

  private static double[] vCopy(double[] vector, double[] srcVec) {
    vector[0] = srcVec[0];
    vector[1] = srcVec[1];
    vector[2] = srcVec[2];
    return vector;
  }

  private static double[] vNorm(double[] vector) {
    double length = Math.sqrt(vProd(vector, vector));
    vector[0] /= length;
    vector[1] /= length;
    vector[2] /= length;
    return vector;
  }

  private static double[] vScale(double[] vector, double value) {
    vector[0] *= value;
    vector[1] *= value;
    vector[2] *= value;
    return vector;
  }

  private static double vProd(double[] vec1, double[] vec2) {
    return vec1[0] * vec2[0] + vec1[1] * vec2[1] + vec1[2] * vec2[2];
  }

  private static double[] vAdd(double[] vector, double[] srcVec) {
    vector[0] += srcVec[0];
    vector[1] += srcVec[1];
    vector[2] += srcVec[2];
    return vector;
  }

  private static double[] vSub(double[] vector, double[] srcVec) {
    vector[0] -= srcVec[0];
    vector[1] -= srcVec[1];
    vector[2] -= srcVec[2];
    return vector;
  }

  private static double[] vMul(double[] vector, double[] vec1, double[] vec2) {
    vector[0] = vec1[1] * vec2[2] - vec1[2] * vec2[1];
    vector[1] = vec1[2] * vec2[0] - vec1[0] * vec2[2];
    vector[2] = vec1[0] * vec2[1] - vec1[1] * vec2[0];
    return vector;
  }

  private static double[] vRotX(double[] vector, double angle) {
    double sinA = Math.sin(angle);
    double cosA = Math.cos(angle);
    double y = vector[1] * cosA - vector[2] * sinA;
    double z = vector[1] * sinA + vector[2] * cosA;
    vector[1] = y;
    vector[2] = z;
    return vector;
  }

  private static double[] vRotY(double[] vector, double angle) {
    double sinA = Math.sin(angle);
    double cosA = Math.cos(angle);
    double x = vector[0] * cosA - vector[2] * sinA;
    double z = vector[0] * sinA + vector[2] * cosA;
    vector[0] = x;
    vector[2] = z;
    return vector;
  }

  private static double[] vRotZ(double[] vector, double angle) {
    double sinA = Math.sin(angle);
    double cosA = Math.cos(angle);
    double x = vector[0] * cosA - vector[1] * sinA;
    double y = vector[0] * sinA + vector[1] * cosA;
    vector[0] = x;
    vector[1] = y;
    return vector;
  }
}
