/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.hadoop.examples.dancing;

import java.util.*;

public class Pentomino {
  public static final String DEPTH = "mapreduce.pentomino.depth";
  public static final String WIDTH = "mapreduce.pentomino.width";
  public static final String HEIGHT = "mapreduce.pentomino.height";
  public static final String CLASS = "mapreduce.pentomino.class";

  /**
   * This interface just is a marker for what types I expect to get back
   * as column names.
   */
  protected static interface ColumnName {
    // NOTHING
  }

  /**
   * Maintain information about a puzzle piece.
   */
  protected static class Piece implements ColumnName {
    private String name;
    private boolean [][] shape;
    private int[] rotations;
    private boolean flippable;
    
    public Piece(String name, String shape, 
                 boolean flippable, int[] rotations) {
      this.name = name;
      this.rotations = rotations;
      this.flippable = flippable;
      StringTokenizer parser = new StringTokenizer(shape, "/");
      List<boolean[]> lines = new ArrayList<boolean[]>();
      while (parser.hasMoreTokens()) {
        String token = parser.nextToken();
        boolean[] line = new boolean[token.length()];
        for(int i=0; i < line.length; ++i) {
          line[i] = token.charAt(i) == 'x';
        }
        lines.add(line);
      }
      this.shape = new boolean[lines.size()][];
      for(int i=0 ; i < lines.size(); i++) {
        this.shape[i] = lines.get(i);
      }
    }
    
    public String getName() {
      return name;
    }
    
    public int[] getRotations() {
      return rotations.clone();
    }
    
    public boolean getFlippable() {
      return flippable;
    }
    
    private int doFlip(boolean flip, int x, int max) {
      if (flip) {
        return max - x - 1;
      } else {
        return x;
      }
    }
    
    public boolean[][] getShape(boolean flip, int rotate) {
      boolean [][] result;
      if (rotate % 2 == 0) {
        int height = shape.length;
        int width = shape[0].length;
        result = new boolean[height][];
        boolean flipX = rotate == 2;
        boolean flipY = flip ^ (rotate == 2);
        for (int y = 0; y < height; ++y) {
          result[y] = new boolean[width];
          for (int x=0; x < width; ++x) {
            result[y][x] = shape[doFlip(flipY, y, height)]
                                 [doFlip(flipX, x, width)];
          }
        }
      } else {
        int height = shape[0].length;
        int width = shape.length;
        result = new boolean[height][];
        boolean flipX = rotate == 3;
        boolean flipY = flip ^ (rotate == 1);
        for (int y = 0; y < height; ++y) {
          result[y] = new boolean[width];
          for (int x=0; x < width; ++x) {
            result[y][x] = shape[doFlip(flipX, x, width)]
                                 [doFlip(flipY, y, height)];
          }
        }        
      }
      return result;
    }
  }

  /**
   * A point in the puzzle board. This represents a placement of a piece into
   * a given point on the board.
   */
  static class Point implements ColumnName {
    int x;
    int y;
    Point(int x, int y) {
      this.x = x;
      this.y = y;
    }
  }
  

  /**
   * Convert a solution to the puzzle returned by the model into a string
   * that represents the placement of the pieces onto the board.
   * @param width the width of the puzzle board
   * @param height the height of the puzzle board
   * @param solution the list of column names that were selected in the model
   * @return a string representation of completed puzzle board
   */
  public static String stringifySolution(int width, int height, 
                                         List<List<ColumnName>> solution) {
    String[][] picture = new String[height][width];
    StringBuffer result = new StringBuffer();
    // for each piece placement...
    for(List<ColumnName> row: solution) {
      // go through to find which piece was placed
      Piece piece = null;
      for(ColumnName item: row) {
        if (item instanceof Piece) {
          piece = (Piece) item;
          break;
        }
      }
      // for each point where the piece was placed, mark it with the piece name
      for(ColumnName item: row) {
        if (item instanceof Point) {
          Point p = (Point) item;
          picture[p.y][p.x] = piece.getName();
        }
      }
    }
    // put the string together
    for(int y=0; y < picture.length; ++y) {
      for (int x=0; x < picture[y].length; ++x) {
        result.append(picture[y][x]);
      }
      result.append("\n");
    }
    return result.toString();
  }
  
  public enum SolutionCategory {UPPER_LEFT, MID_X, MID_Y, CENTER}
  
  /**
   * Find whether the solution has the x in the upper left quadrant, the
   * x-midline, the y-midline or in the center.
   * @param names the solution to check
   * @return the catagory of the solution
   */
  public SolutionCategory getCategory(List<List<ColumnName>> names) {
    Piece xPiece = null;
    // find the "x" piece
    for(Piece p: pieces) {
      if ("x".equals(p.name)) {
        xPiece = p;
        break;
      }
    }
    // find the row containing the "x"
    for(List<ColumnName> row: names) {
      if (row.contains(xPiece)) {
        // figure out where the "x" is located
        int low_x = width;
        int high_x = 0;
        int low_y = height;
        int high_y = 0;
        for(ColumnName col: row) {
          if (col instanceof Point) {
            int x = ((Point) col).x;
            int y = ((Point) col).y;
            if (x < low_x) {
              low_x = x;
            }
            if (x > high_x) {
              high_x = x;
            }
            if (y < low_y) {
              low_y = y;
            }
            if (y > high_y) {
              high_y = y;
            }
          }
        }
        boolean mid_x = (low_x + high_x == width - 1);
        boolean mid_y = (low_y + high_y == height - 1);
        if (mid_x && mid_y) {
          return SolutionCategory.CENTER;
        } else if (mid_x) {
          return SolutionCategory.MID_X;
        } else if (mid_y) {
          return SolutionCategory.MID_Y;
        }
        break;
      }
    }
    return SolutionCategory.UPPER_LEFT;
  }
  
  /**
   * A solution printer that just writes the solution to stdout.
   */
  private static class SolutionPrinter 
                       implements DancingLinks.SolutionAcceptor<ColumnName> {
    int width;
    int height;
    
    public SolutionPrinter(int width, int height) {
      this.width = width;
      this.height = height;
    }
    
    public void solution(List<List<ColumnName>> names) {
      System.out.println(stringifySolution(width, height, names));
    }
  }
  
  protected int width;
  protected int height;

  protected List<Piece> pieces = new ArrayList<Piece>();
  
  /**
   * Is the piece fixed under rotation?
   */
  protected static final int [] oneRotation = new int[]{0};
  
  /**
   * Is the piece identical if rotated 180 degrees?
   */
  protected static final int [] twoRotations = new int[]{0,1};
  
  /**
   * Are all 4 rotations unique?
   */
  protected static final int [] fourRotations = new int[]{0,1,2,3};
  
  /**
   * Fill in the pieces list.
   */
  protected void initializePieces() {
    pieces.add(new Piece("x", " x /xxx/ x ", false, oneRotation));
    pieces.add(new Piece("v", "x  /x  /xxx", false, fourRotations));
    pieces.add(new Piece("t", "xxx/ x / x ", false, fourRotations));
    pieces.add(new Piece("w", "  x/ xx/xx ", false, fourRotations));
    pieces.add(new Piece("u", "x x/xxx", false, fourRotations));
    pieces.add(new Piece("i", "xxxxx", false, twoRotations));
    pieces.add(new Piece("f", " xx/xx / x ", true, fourRotations));
    pieces.add(new Piece("p", "xx/xx/x ", true, fourRotations));
    pieces.add(new Piece("z", "xx / x / xx", true, twoRotations));
    pieces.add(new Piece("n", "xx  / xxx", true, fourRotations));
    pieces.add(new Piece("y", "  x /xxxx", true, fourRotations));
    pieces.add(new Piece("l", "   x/xxxx", true, fourRotations));
  }
  
  /**
   * Is the middle of piece on the upper/left side of the board with 
   * a given offset and size of the piece? This only checks in one
   * dimension.
   * @param offset the offset of the piece
   * @param shapeSize the size of the piece
   * @param board the size of the board
   * @return is it in the upper/left?
   */
  private static boolean isSide(int offset, int shapeSize, int board) {
    return 2*offset + shapeSize <= board;
  }
  
  /**
   * For a given piece, generate all of the potential placements and add them 
   * as rows to the model.
   * @param dancer the problem model
   * @param piece the piece we are trying to place
   * @param width the width of the board
   * @param height the height of the board
   * @param flip is the piece flipped over?
   * @param row a workspace the length of the each row in the table
   * @param upperLeft is the piece constrained to the upper left of the board?
   *        this is used on a single piece to eliminate most of the trivial
   *        roations of the solution.
   */
  private static void generateRows(DancingLinks dancer,
                                   Piece piece,
                                   int width,
                                   int height,
                                   boolean flip,
                                   boolean[] row,
                                   boolean upperLeft) {
    // for each rotation
    int[] rotations = piece.getRotations();
    for(int rotIndex = 0; rotIndex < rotations.length; ++rotIndex) {
      // get the shape
      boolean[][] shape = piece.getShape(flip, rotations[rotIndex]);
      // find all of the valid offsets
      for(int x=0; x < width; ++x) {
        for(int y=0; y < height; ++y) {
          if (y + shape.length <= height && x + shape[0].length <= width &&
              (!upperLeft || 
                  (isSide(x, shape[0].length, width) && 
                   isSide(y, shape.length, height)))) {
            // clear the columns related to the points on the board
            for(int idx=0; idx < width * height; ++idx) {
              row[idx] = false;
            }
            // mark the shape
            for(int subY=0; subY < shape.length; ++subY) {
              for(int subX=0; subX < shape[0].length; ++subX) {
                row[(y + subY) * width + x + subX] = shape[subY][subX];
              }
            }
            dancer.addRow(row);
          }         
        }
      }
    }
  }
  
  private DancingLinks<ColumnName> dancer = new DancingLinks<ColumnName>();
  private DancingLinks.SolutionAcceptor<ColumnName> printer;
  
  {
    initializePieces();
  }

  /**
   * Create the model for a given pentomino set of pieces and board size.
   * @param width the width of the board in squares
   * @param height the height of the board in squares
   */
  public Pentomino(int width, int height) {
    initialize(width, height);
  }

  /**
   * Create the object without initialization.
   */
  public Pentomino() {
  }

  void initialize(int width, int height) {
    this.width = width;
    this.height = height;
    for(int y=0; y < height; ++y) {
      for(int x=0; x < width; ++x) {
        dancer.addColumn(new Point(x,y));
      }
    }
    int pieceBase = dancer.getNumberColumns();
    for(Piece p: pieces) {
      dancer.addColumn(p);
    }
    boolean[] row = new boolean[dancer.getNumberColumns()];
    for(int idx = 0; idx < pieces.size(); ++idx) {
      Piece piece = pieces.get(idx);
      row[idx + pieceBase] = true;
      generateRows(dancer, piece, width, height, false, row, idx == 0);
      if (piece.getFlippable()) {
        generateRows(dancer, piece, width, height, true, row, idx == 0);
      }
      row[idx + pieceBase] = false;
    }
    printer = new SolutionPrinter(width, height);
  }
  
  /**
   * Generate a list of prefixes to a given depth
   * @param depth the length of each prefix
   * @return a list of arrays of ints, which are potential prefixes
   */
  public List<int[]> getSplits(int depth) {
    return dancer.split(depth);
  }
  
  /**
   * Find all of the solutions that start with the given prefix. The printer
   * is given each solution as it is found.
   * @param split a list of row indexes that should be choosen for each row
   *        in order
   * @return the number of solutions found
   */
  public int solve(int[] split) {
    return dancer.solve(split, printer);
  }
  
  /**
   * Find all of the solutions to the puzzle.
   * @return the number of solutions found
   */
  public int solve() {
    return dancer.solve(printer);
  }
  
  /**
   * Set the printer for the puzzle.
   * @param printer A call-back object that is given each solution as it is 
   * found.
   */
  public void setPrinter(DancingLinks.SolutionAcceptor<ColumnName> printer) {
    this.printer = printer;
  }
  
  /**
   * Solve the 6x10 pentomino puzzle.
   */
  public static void main(String[] args) {
    int width = 6;
    int height = 10;
    Pentomino model = new Pentomino(width, height);
    List splits = model.getSplits(2);
    for(Iterator splitItr=splits.iterator(); splitItr.hasNext(); ) {
      int[] choices = (int[]) splitItr.next();
      System.out.print("split:");
      for(int i=0; i < choices.length; ++i) {
        System.out.print(" " + choices[i]);
      }
      System.out.println();
      
      System.out.println(model.solve(choices) + " solutions found.");
    }
  }

}
