/* 
 * Pescetti Pseudo-Duplimate Generator
 * 
 * Copyright (C) 2007 Matthew Johnson
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License Version 2 as published by
 * the Free Software Foundation.  This program is distributed in the hope that
 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.  You should have received a
 * copy of the GNU General Public License along with this program; if not,
 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA  02111-1307, USA.
 *
 * To Contact me, please email src@matthew.ath.cx
 *
 */

package cx.ath.matthew.pescetti;

import cx.ath.matthew.debug.Debug;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import java.util.Vector;

import static cx.ath.matthew.pescetti.Gettext._;

public class pescetti implements Constants
{
   private static SecureRandom sr = new SecureRandom();
   public static void swap(byte[] perm, int a, int b)
   {
      byte t = perm[a];
      perm[a] = perm[b];
      perm[b] = t;
   }

   public static String formatDeck(byte[][] deck)
   {
      StringBuffer b = new StringBuffer();
      if (Debug.debug) {
         for (int i = 0; i < deck.length; i++)
            for (int j = 0; j < deck[i].length; j++) {
               b.append(CARD[deck[i][j] & 0x0f]);
               b.append(SUIT[deck[i][j] >> 4]);
               b.append(' ');
            }
         return b.toString();
      } else { return ""; }
   }

   /**
    * Generates a part 1 permutation.
    * Result is 10 random numbers between 3 and 8 which sum to 52.
    */
   public static byte[][] generatePermutation1()
   {
      // number in each pile
      byte[] res = new byte[10];
      int rem = 52;
      for (int i = 0; i < 10; i++) {
         int top = Math.min(8, rem - 3*(10-(i+1)));
         int bottom = Math.max(3, rem - 8*(10-(i+1)));
         int range = top - bottom + 1;
         int rand = sr.nextInt() % range;
         if (rand < 0) rand = -rand;
         res[i] = (byte) (bottom + rand);
         rem -= res[i];
      }
      if (Debug.debug)  Debug.print(Debug.DEBUG, "First perm: "+Arrays.toString(res));

      // pickup order
      byte[] res2 = new byte[10];
      for (byte i = 0; i < 10; i++) 
         res2[i] = (byte) (i+1);
      
      // knuth shuffle
      byte[] rand = new byte[10];
      sr.nextBytes(rand);
      for (byte k = 0; k < 10; k++)
         swap(res2, k, Math.abs(rand[k]) % 10);

      if (Debug.debug)  Debug.print(Debug.DEBUG, "Pickup: "+Arrays.toString(res2));
      return new byte[][] { res, res2 };
   }

   /**
    * Generates a random card to cut to.
    */
   public static int getCutPoint()
   {
      int s = sr.nextInt() % 4;
      int c = sr.nextInt() % KING;
      if (s < 0) s = -s;
      if (c < 0) c = -c;
      s += 1;
      c += 2;
      if (Debug.debug) Debug.print(Debug.DEBUG, "Cut point: suit="+s+", card="+c);
      return (s << 4) | c;
   }

   /**
    * Returns a deck cut to the given position.
    */
   public static byte[][] cutDeck(byte[][] deck, int cutpoint)
   {
      if (Debug.debug) Debug.print(Debug.DEBUG, "Old deck: "+formatDeck(deck));
      if (Debug.debug) Debug.print(Debug.DEBUG, "Cut point: "+cutpoint);

      byte[][] newdeck = new byte[4][13];
      int i1, j1, i2, j2, i3, j3;
      i1 = j1 = i2 = j2 = i3 = j3 = 0;

      // find the cut point
      while (i1 < deck.length && deck[i1][j1] != cutpoint) {
         if (j1 < (deck[i1].length-1)) j1++;
         else {
            j1 = 0;
            i1++;
         }
      }
      i3 = i1;
      j3 = j1;

      // actually we want c last
      if (j1 < (deck[i1].length-1)) j1++;
      else {
         j1 = 0;
         i1++;
      }

      // copy c=>end to 0=>c in new deck
      while (i1 < deck.length) {
         newdeck[i2][j2] = deck[i1][j1];
         if (j1 < (deck[i1].length-1)) j1++;
         else {
            j1 = 0;
            i1++;
         }
         if (j2 < (newdeck[i2].length-1)) j2++;
         else {
            j2 = 0;
            i2++;
         }
      }
      
      if (Debug.debug) Debug.print(Debug.VERBOSE, "Got i1="+i1+", j1="+j1+", i2="+i2+", j2="+j2+", i3="+i3+", j3="+j3);
      if (Debug.debug) Debug.print(Debug.DEBUG, "Intermediate deck: "+formatDeck(newdeck));

      // copy 0=>c to c=>end in new deck
      i1 = j1 = 0;
      while (i1 < i3 || (i1 == i3 && j1 <= j3)) {
         newdeck[i2][j2] = deck[i1][j1];
         if (j1 < (deck[i1].length-1)) j1++;
         else {
            j1 = 0;
            i1++;
         }
         if (j2 < (newdeck[i2].length-1)) j2++;
         else {
            j2 = 0;
            i2++;
         }
      }

      if (Debug.debug) Debug.print(Debug.DEBUG, "New deck: "+formatDeck(newdeck));

      return newdeck;
   }

   /**
    * Returns a deck permuted with a perm1 permutation.
    */
   public static byte[][] permute1(byte[][] deck, byte[][] perm1)
   {
      if (Debug.debug) Debug.print(Debug.DEBUG, "deck: "+formatDeck(deck));
      if (Debug.debug) Debug.print(Debug.DEBUG, "perm: "+formatDeck(perm1));

      // get the sizes of the piles
      byte[][] intermediate = new byte[perm1[0].length][];
      for (int i = 0; i < intermediate.length; i++)
         intermediate[i] = new byte[perm1[0][i]];

      int i1, j1, i2, j2;
      i1 = j1 = i2 = j2 = 0;

      // copy into 10 piles, inverting as you do
      j2 = intermediate[i2].length-1;
      while (i1 < deck.length) {
         intermediate[i2][j2] = deck[i1][j1];
         if (j2 > 0) j2--;
         else {
            i2++;
            if (i2 < intermediate.length)
               j2 = intermediate[i2].length-1;
         }
         if (j1 < (deck[i1].length-1)) j1++;
         else {
            i1++;
            j1 = 0;
         }
      }


      if (Debug.debug) Debug.print(Debug.DEBUG, "Intermediate: "+formatDeck(intermediate));

      byte[][] newdeck = new byte[4][13];

      int o = 0;
      j1 = i2 = j2 = 0;
      i1 = perm1[1][o]-1;

      // copy back from ten piles in random order
      while (i2 < newdeck.length) {
         newdeck[i2][j2] = intermediate[i1][j1];
         if (j2 < (newdeck[i2].length-1)) j2++;
         else {
            i2++;
            j2 = 0;
         }
         if (j1 < (intermediate[i1].length-1)) j1++;
         else {
            // select the next random pile
            o++;
            if (o >= perm1[1].length) break;
            i1 = perm1[1][o]-1;
            j1 = 0;
         }
      }

      if (Debug.debug) Debug.print(Debug.DEBUG, "New deck: "+formatDeck(newdeck));

      return newdeck;
   }

   /**
    * Generates a random permutaion using Knuth's Shuffle.
    * Start with 11111.....22222....33333...444444..., then
    * for each item in the list 0....51 swap it with a randomly
    * chosen item in the list 0....51.
    * the parameter changes the mode from have 13 of each number 
    * to having between 10 and 16 of each number
    */
   public static byte[] generatePermutation(boolean random)
   {
      final int MAX = 16;
      final int MIN = 10;
      final int RANGE = MAX-MIN;
      byte[] perm = new byte[52];
      byte[] rand = new byte[52];
      if (random) { 
         int[] d = new int[4];
         d[0] = MIN + (Math.abs(sr.nextInt()) % RANGE);
         d[1] = MIN + (Math.abs(sr.nextInt()) % RANGE);
         if (sr.nextBoolean()) {
            d[2] = 26-d[0];
            d[3] = 26-d[1];
         } else {
            d[2] = 26-d[1];
            d[3] = 26-d[0];
         }

         for (byte i = 0, k = 0; i < 4; i++) 
            for (byte j = 0; j < d[i]; j++, k++) 
               perm[k] = i;
      } else {
         for (byte i = 0, k = 0; i < 4; i++)
            for (byte j = 0; j < 13; j++, k++) 
               perm[k] = i;
      }
      sr.nextBytes(rand);
      for (byte k = 0; k < 52; k++)
         swap(perm, k, Math.abs(rand[k]) % 52);
      return perm;
   }
   /**
    * Takes a set of hands and a permutation and generates the permuted hands.
    */
   public static byte[][] permute(byte[][] hands, byte[] permutation)
   {
      Vector[] newhandv = new Vector[] { new Vector(), new Vector(),
         new Vector(), new Vector() };
      for (int i = 0, k = 0; i < 4; i++)
         for (int j = 0; j < hands[i].length; j++, k++) 
            newhandv[permutation[k]].add(hands[i][j]);

      byte[][] newhands = new byte[4][];
      for (int i = 0; i < 4; i++) {
         newhands[i] = new byte[newhandv[i].size()];
         for (int j = 0; j < newhands[i].length; j++)
            newhands[i][j] = (Byte) newhandv[i].get(j);
      }

      return reverse(newhands);
   }

	@SuppressWarnings("fallthrough")
   public static double[] getStats(byte[][][] boards, byte[][] tricks)
   {
      // { NS games, EW games, NS slams, EW slams, 
      //   N points, E points, S points, W points,
      //   4333%, bal%, 6+ count, 7+ count, 8+ count }
      double[] stats = new double[STAT_MAX];
      
      for (int i = 0; i < boards.length; i++) {
         boolean ewslam=false;
         boolean ewgame=false;
         boolean nsslam=false;
         boolean nsgame=false;
         if (null != tricks) {
            if (tricks[i][0+0] >= 9 ||
                tricks[i][0+1] >= 10 ||
                tricks[i][0+2] >= 10 ||
                tricks[i][0+3] >= 11 ||
                tricks[i][0+4] >= 11 ||
                tricks[i][5+0] >= 9 ||
                tricks[i][5+1] >= 10 ||
                tricks[i][5+2] >= 10 ||
                tricks[i][5+3] >= 11 ||
                tricks[i][5+4] >= 11) if (!nsgame) { nsgame=true; stats[STAT_NSGAMES]++; }
            if (tricks[i][10+0] >= 9 ||
                tricks[i][10+1] >= 10 ||
                tricks[i][10+2] >= 10 ||
                tricks[i][10+3] >= 11 ||
                tricks[i][10+4] >= 11 ||
                tricks[i][15+0] >= 9 ||
                tricks[i][15+1] >= 10 ||
                tricks[i][15+2] >= 10 ||
                tricks[i][15+3] >= 11 ||
                tricks[i][15+4] >= 11) if (!ewgame) { ewgame=true; stats[STAT_EWGAMES]++; }
            for (int j = 0; j < 10; j++)
               if (!nsslam && tricks[i][j] >= 12) { nsslam=true; stats[STAT_NSSLAMS]++; }
            for (int j = 10; j < 20; j++) 
               if (!ewslam && tricks[i][j] >= 12) { ewslam=true; stats[STAT_EWSLAMS]++; }
            }
         stats[STAT_NPOINTS] += count(boards[i][NORTH]);
         stats[STAT_SPOINTS] += count(boards[i][SOUTH]);
         stats[STAT_EPOINTS] += count(boards[i][EAST]);
         stats[STAT_WPOINTS] += count(boards[i][WEST]);
         if (bacon(boards[i][NORTH]) ||
             bacon(boards[i][SOUTH]) ||
             bacon(boards[i][WEST]) ||
             bacon(boards[i][EAST]))
            stats[STAT_BACON]++;

         for (int p = 0; p < 4; p++) {
            boolean flat = true;
            boolean bal = true;
            boolean semibal = true;
            int xton = 0;
            int five = 0;
            int four = 0;
            for (int s = 0; s < 4; s++) {
               if (Debug.debug) Debug.print(Debug.VERBOSE, "board "+i+" player "+p+" suit "+s+" length="+length(boards[i][p], s));
               switch (length(boards[i][p], s)) {
                  case 13:
                  case 12:
                  case 11:
                  case 10:
                  case 9:
                  case 8:
                     stats[STAT_8PLUS]++;
                  case 7:
                     stats[STAT_7PLUS]++;
                  case 6:
                     stats[STAT_6PLUS]++;
                     flat = false;
                     break;
                  case 5:
                     five++;
                     flat = false;
                     break;
                  case 4:
                     four++;
                  case 3:
                     break;
                  case 2:
                     xton++;
                     flat = false;
                     break;
                  case 0:
                     stats[STAT_VOID]++;
                  case 1:
                     bal = false;
                     semibal = false;
                     flat = false;
                     break;
               }
            }
            if (Debug.debug) Debug.print(Debug.DEBUG, "board "+i+" player "+p+" xton= "+xton+" four="+four+" five="+five);
            if (xton > 1) bal = false;
            if (xton > 2) semibal = false;
            if (flat) stats[STAT_4333]++;
            if (bal) stats[STAT_BAL]++;
            if (semibal) stats[STAT_SEMIBAL]++;
            if (five == 2) stats[STAT_55]++;
            if (four == 3) stats[STAT_4441]++;
            if (Debug.debug) Debug.print(Debug.DEBUG, "stat_4441:"+stats[STAT_4441] );
         }
      }

      // get average
      stats[STAT_NPOINTS] /=  boards.length;
      stats[STAT_SPOINTS] /=  boards.length;
      stats[STAT_EPOINTS] /=  boards.length;
      stats[STAT_WPOINTS] /=  boards.length;

      return stats;
   }
	@SuppressWarnings("fallthrough")
   public static int count(byte[] hand)
   {
      int total = 0;
      for (byte a: hand) {
         int b = a & 0x0f;
         if (Debug.debug) Debug.print(Debug.VERBOSE,"a = "+a+" b = "+b+" / "+CARD[b]);
         switch (b) {
            case ACE: total++;
            case KING: total++;
            case QUEEN: total++;
            case JACK: total++;
         }
      }
      if (Debug.debug) Debug.print(Debug.DEBUG,"total="+total);
      return total;
   }

   public static byte[] generateInitialPermutation()
   {
      // returns a permutation of the four suits.
      byte[] initperm = new byte[] { SPADES, HEARTS, DIAMONDS, CLUBS };
      byte[] rand = new byte[4];
      sr.nextBytes(rand);
      for (byte i = 0; i < 4; i++)
         swap(initperm, i, Math.abs(rand[i]) % 4);
      return initperm;
   }
   public static byte[][] generateInitialHand(byte[] initperm)
   {
      byte[][] hand = new byte[4][];
      for (int i = 0; i < 4; i++)
         switch (initperm[i]) {
            case SPADES:
               hand[i] = INITIAL_SPADES;
               break;
            case HEARTS:
               hand[i] = INITIAL_HEARTS;
               break;
            case DIAMONDS:
               hand[i] = INITIAL_DIAMONDS;
               break;
            case CLUBS:
               hand[i] = INITIAL_CLUBS;
               break;
         }
      return hand;
   }

   /** 
    * Reverses the contents of each hand
    */
   public static byte[][] reverse(byte[][] hands)
   {
      byte[][] newhands = new byte[4][];
      for (int i = 0; i < 4; i++) {
         newhands[i] = new byte[hands[i].length];
         for (int j = 0, k = (hands[i].length - 1); j < hands[i].length; j++, k--)
            newhands[i][k] = hands[i][j];
      }
      return newhands;
   }

   public static byte find(byte[][] hands, byte card)
   {
      for (byte i = 0; i < 4; i++)
         for (int j = 0; j < hands[i].length; j++) 
            if (hands[i][j] == card)
               return i;
      return 0; // never happens
   }

   /**
    * Takes 2 hands and returns the permutation between them.
    */
   public static byte[] getPermutation(byte[][] hand1, byte[][] hand2)
   {
      byte[] perm = new byte[52];
      for (int i = 0, k = 0; i < 4; i++)
         for (int j = 0; j < hand1[i].length; j++, k++) 
            perm[k] = find(hand2, hand1[i][j]);
      return perm;
   }

   /**
    * import from format used by dds double dummy solver.
    * W N E S = 3 0 1 2
    * A64.8732.543.Q98 T8.96.AKT.A76432 Q2.AQJT54.Q82.JT KJ9753.K.J976.K5
    */
   public static byte[][][] ddsimport(InputStream in) throws IOException
   {
      Vector/*<byte[][]>*/ hands = new Vector/*<byte[][]>*/();
      BufferedReader br = new BufferedReader(new InputStreamReader(in));
      String s;
      while (null != (s = br.readLine()))
         hands.add(ddsinparse(s));
      return (byte[][][]) hands.toArray(new byte[0][][]);
   }

   public static byte[][] ddsinparse(String str)
   {
      byte[][] hand = new byte[4][13]; //start afresh
      int s=1;
      int p=3; // start at west
      int i=0;
      for (int j = 0; j < str.length(); j++) {
         char c = str.charAt(j);
         if (Debug.debug) Debug.print(Debug.DEBUG, "Read c="+c+" s="+s+" p="+p+" i="+i);
         switch (c) {
            case '.':
               s++; // next suit
               i--; // oops, didn't actually want to increment this
               break;
            case ' ':
               p = (p + 1) % 4; // next player
               s=1; // start suits again
               i=-1; // start cards in hand again
               break;
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
               hand[p][i] = (byte) (c - '1' + 1 + (s << 4));
               break;
            case 'T':
               hand[p][i] = (byte) (TEN + (s << 4));
               break;
            case 'J':
               hand[p][i] = (byte) (JACK + (s << 4));
               break;
            case 'Q':
               hand[p][i] = (byte) (QUEEN + (s << 4));
               break;
            case 'K':
               hand[p][i] = (byte) (KING + (s << 4));
               break;
            case 'A':
               hand[p][i] = (byte) (ACE + (s << 4));
               break;
            default:
         }
         i++; // next card in hand
      }
      return hand;
   }

   public static void exportanalysis(PrintStream out, byte[][] tricks) 
   {
      for (int i = 0; i < tricks.length; i++) {
         printtricks(out, tricks[i]);
         out.println();
      }
   }
   public static void combinedexport(PrintStream out, byte[][][] boards, byte[][] tricks) throws Exception
   {
      if (null == tricks) throw new Exception(_("Must --load, --load-analysis or --analyze to use --save"));
      for (int i = 0; i < boards.length; i++) {
         ddsprinthand(out, boards[i][WEST]);
         out.print(' ');
         ddsprinthand(out, boards[i][NORTH]);
         out.print(' ');
         ddsprinthand(out, boards[i][EAST]);
         out.print(' ');
         ddsprinthand(out, boards[i][SOUTH]);
         out.print(':');
         printtricks(out, tricks[i]);
         out.println();
      }
   }
   public static byte[] analysisparse(String s)
   {
      String[] ss = s.split(",");
      byte[] tricks = new byte[ss.length];
      for (int i = 0; i < ss.length; i++)
         tricks[i] = Byte.parseByte(ss[i]);
      return tricks;
   }
   public static void printtricks(PrintStream out, byte[] tricks)
   {
      for (int i = 0; i < tricks.length; i++) {
         out.print(tricks[i]);
         if ((i+1) != tricks.length) out.print(',');
      }
   }
   public static byte[][] importanalysis(InputStream in) throws IOException
   {
      Vector/*<byte[]>*/ tricks = new Vector/*<byte[]>*/();
      BufferedReader br = new BufferedReader(new InputStreamReader(in));
      String s;
      while (null != (s = br.readLine()))
         tricks.add(analysisparse(s));
      return (byte[][]) tricks.toArray(new byte[0][]);
   }
   public static Object[] combinedimport(InputStream in) throws IOException
   {
      Vector/*<byte[][]>*/ hands = new Vector/*<byte[][]>*/();
      Vector/*<byte[]>*/ tricks = new Vector/*<byte[]>*/();
      BufferedReader br = new BufferedReader(new InputStreamReader(in));
      String s;
      while (null != (s = br.readLine())) {
         String[] ss = s.split(":");
         hands.add(ddsinparse(ss[0]));
         tricks.add(analysisparse(ss[1]));
      }
      return new Object[] { hands.toArray(new byte[0][][]),
         tricks.toArray(new byte[0][]) };
   }
   /**
    * Export to format used by dds double dummy solver.
    * W N E S = 3 0 1 2
    * A64.8732.543.Q98 T8.96.AKT.A76432 Q2.AQJT54.Q82.JT KJ9753.K.J976.K5
    *
    * dds only solves for N-S, so we have to output two lines, one of them rotated for E-W (rotation printed if rotate is true)
    *
    */
   public static void ddsexport(PrintStream out, byte[][] hands, boolean rotate)
   {
      for (int i = 0; i < 4; i++) Arrays.sort(hands[i]);
      if (!rotate) {
         ddsprinthand(out, hands[WEST]);
         out.print(' ');
         ddsprinthand(out, hands[NORTH]);
         out.print(' ');
         ddsprinthand(out, hands[EAST]);
         out.print(' ');
         ddsprinthand(out, hands[SOUTH]);
         out.println();
      } else {
         ddsprinthand(out, hands[NORTH]);
         out.print(' ');
         ddsprinthand(out, hands[EAST]);
         out.print(' ');
         ddsprinthand(out, hands[SOUTH]);
         out.print(' ');
         ddsprinthand(out, hands[WEST]);
         out.println();
      }
   }
   public static void ddsprinthand(PrintStream out, byte[] hand)
   {
      int suit = SPADES;
      for (int s = 1, i = 0; s <= 4; s++) {
         while (i < 13 && s == (hand[i] >> 4))
            out.print(CARD[hand[i++] & 0x0f]);
         if (s < 4) out.print('.');
      }
   }
   
   static void syntax()
   {
      String version = Package.getPackage("cx.ath.matthew.pescetti")
                              .getImplementationVersion();
      System.out.println("Pescetti Pseudo-Duplimate Generator - version "+version);
      System.out.println("Syntax: pescetti [options]");
      System.out.println("   Options: --help --generate=N --load=<boards.txt> --load-dds=<boards.dds> --load-analysis=<tricks.txt> --permutations=<permutations.txt> --demo --stats --analyze --save=<boards.txt> --save-dds=<boards.dds> --save-analysis=<tricks.txt> --format=<format> --title=<title> --output=<hands.txt> --curtains=<cutains.txt> --probability=<factor> --criteria=<criteria list>");
      System.out.println("   Formats: txt html pdf");
      System.out.println("   Criteria: unbalanced weaknt strongnt twont strongtwo weaktwo three twoclubs 4441 singlesuit twosuits partscore game slam grand game-invite slam-invite jumpshift jumpfit splinter bacon weird");
   }

   public static byte[] ddanalyse(byte[][] hand)
   {
      try {
         File exec = new File("/usr/games/dds");
         if (!exec.exists()) throw new Exception(_("Must install dds double dummy analyzer to analyze hands"));
         File temp = File.createTempFile("pescetti", ".tmp");
         PrintStream fos = new PrintStream(new FileOutputStream(temp));
         File temp2 = File.createTempFile("pescetti", ".tmp");
         ddsexport(fos, hand, false);
         ddsexport(fos, hand, true);
         fos.close();
         Process p = Runtime.getRuntime().exec(new String[] { "/bin/sh", "-c", "/usr/games/dds -deal=1 -tricks "+temp.getCanonicalPath()+" | /usr/bin/tail -n2 > " +temp2.getCanonicalPath() });
         p.waitFor();
         p = Runtime.getRuntime().exec(new String[] { "/bin/sh", "-c", "/usr/games/dds -deal=2 -tricks "+temp.getCanonicalPath()+" | /usr/bin/tail -n2 >> " +temp2.getCanonicalPath()});
         p.waitFor();
         BufferedInputStream in = new BufferedInputStream(new FileInputStream(temp2));
         byte[] tricks = parseDD(in);
         temp.delete();
         temp2.delete();
         return tricks;
      } catch (Exception e) {
         if (Debug.debug) Debug.print(Debug.ERR, e);
         System.err.println(_("Failed to DD analyze: ")+e.getMessage());
         System.exit(1);
      }
      return null;
   }

	@SuppressWarnings("fallthrough")
   public static byte[] parseDD(InputStream in) throws IOException
   {
      byte[] out = new byte[20];
      final int START = 1;
      final int READNS = 2;
      final int READEW = 3;
      int c;
      int state = START;
      int suit = -1;
      int offs = 0;
      int n = -1;
      boolean read = false;
      while (0 < (c = in.read())) {
         if (Debug.debug) Debug.print(Debug.DEBUG, ""+(char) c);
         switch (state) {
            case START:
               if (':' == c) state = READNS;
               break;
            case READNS:
            case READEW:
               switch (c) {
                  case '=': suit++; offs=0; if (suit < 5) read = true; break;
                  case '0':
                  case '1':
                  case '2':
                  case '3':
                  case '4':
                  case '5':
                  case '6':
                  case '7':
                  case '8':
                  case '9':
                     if (n < 0) n = c-'0';
                  case 'A':
                  case 'B':
                  case 'C':
                  case 'D':
                     if (n < 0) n = c-'A'+10;
                  case 'a':
                  case 'b':
                  case 'c':
                  case 'd':
                     if (n < 0) n = c-'a'+10;
                     if (read)  {
                        if (Debug.debug) Debug.print(Debug.DEBUG, "state="+state+" c="+((char)c)+" n="+n+" offs="+offs+" suit="+suit);
                        if (READNS == state && 1 == offs)
                           // north gets n tricks in suit
                           out[0 + suit] = (byte) n;
                        else if (READNS == state && 3 == offs)
                           // south gets n tricks in suit
                           out[5 + suit] = (byte) n;
                        else if (READEW == state && 1 == offs)
                           // east gets n tricks in suit
                           out[10 + suit] = (byte) n;
                        else if (READEW == state && 3 == offs)
                           // west gets n tricks in suit
                           out[15 + suit] = (byte) n;
                     }
                     n = -1;
                     offs++;
                     break;
                  case ' ':
                     read = false;
                     break;
                  case ':':
                     state=READEW;
                     suit=-1;
                     break;
                  default:
                     // skip everything else
                     ;
               }
         }
      }
      return out;
   }
   public static void demo()
   {
      Printer print = new TextPrinter(System.out, "Pescetti PseudoDuplimate - Demo mode");
      print.println("Sort decending within each suit, A K .... 3 2 (so that the first card delt is the Ace)");
      print.println("Stack the suits in the order Spades, Hearts, Diamonds, Clubs, so the first suit is spades.");

      int cutpoint = getCutPoint();
      print.print("Cut the deck so that the ");
      print.print(""+CARD[cutpoint & 0x0f]);
      print.print(""+SUIT[cutpoint >> 4]);
      print.println(" is at the bottom.");

      byte[] targetperm = generatePermutation(false);
      byte[][] resulthand = permute(INITIAL_HAND, targetperm);
      byte[][] inithand = cutDeck(INITIAL_HAND, cutpoint);
      byte[][] perm1 = generatePermutation1();
      byte[][] intermediatehand = permute1(inithand, perm1);
      byte[] perm2 = getPermutation(intermediatehand, resulthand);

      print.println("First Deal:");
      print.printPermutation1(1, perm1);
      print.println("Second Deal:");
      print.printPermutation(2, 1, perm2, null);
      print.println("You should now have these hands hand 1 = north, hand 2 = east, hand 3 = south, hand 4 = west");
      print.printHand(1, resulthand);
      print.println("These can go into a file to be run with dds -deal=1 -tricks file.txt and dds -deal=2 -tricks file.txt");
      ddsexport(System.out, resulthand, false);
      ddsexport(System.out, resulthand, true);
      
      byte[] tricks = ddanalyse(resulthand);
      print.println("It says: ");
      print.printTricks(tricks);
      print.close();
      System.exit(0);
   }

   public static void parseopts(HashMap/* <String,String> */ options, String[] args)
   {
      for (int i = 0; i < args.length; i++) {
         String[] opt = args[i].split("=");
         if (Debug.debug) Debug.print(Arrays.asList(opt));
         if (options.containsKey(opt[0])) {
            if (opt.length == 1)
               options.put(opt[0], "true");
            else
               options.put(opt[0], opt[1]);
         } else {
            System.out.println(_("Error: unknown option ")+opt[0]);
            syntax();
            System.exit(1);
         }
      }
   }

   public static void permutation_genandsave(HashMap/* <String,String> */ options, byte[][][] boards)
   {
      String file = (String) options.get("--permutations");
      try {
         PrintStream permfile = new PrintStream(new FileOutputStream(file));
         Printer permprint = null;
         if ("txt".equals(options.get("--format"))) {
            permprint = new TextPrinter(permfile, options.get("--title")+" - "+_("Permutations"));
         } else if ("html".equals(options.get("--format"))) {
            permprint = new HTMLPrinter(permfile, options.get("--title")+" - "+_("Permutations"));
         } else if ("pdf".equals(options.get("--format"))) {
            permprint = new PDFPrinter(permfile, options.get("--title")+" - "+_("Permutations"));
         } else {
            System.err.println(_("Invalid format ")+options.get("--format"));
            System.exit(1);
         }
         int count = boards.length;
         byte[][][] perms = new byte[count][3][];
         // generate them all
         for (int i = 0; i < count; i++) {
            perms[i][0] = generateInitialPermutation();
            byte[][] initial = generateInitialHand(perms[i][0]);
            perms[i][1] = generatePermutation(true);
            byte[][] intermediate = permute(initial, perms[i][1]);
            perms[i][2] = getPermutation(intermediate, boards[i]);
         }
         // print them, interleaving perm1 with perm2 for a later board
         for (int i = 0; i < count; i+=4) {
            if (i+3 < count) {
               permprint.printPermutation(1, i+1, perms[i+0][1], perms[i+0][0]);
               permprint.printPermutation(1, i+2, perms[i+1][1], perms[i+1][0]);
               permprint.printPermutation(2, i+3, perms[i+2][2], null);
               permprint.printPermutation(2, i+4, perms[i+3][2], null);
               permprint.printPermutation(1, i+3, perms[i+2][1], perms[i+2][0]);
               permprint.printPermutation(1, i+4, perms[i+3][1], perms[i+3][0]);
               permprint.printPermutation(2, i+1, perms[i+0][2], null);
               permprint.printPermutation(2, i+2, perms[i+1][2], null);
            } else {
               for (int j = i; j < count; j++) 
                  permprint.printPermutation(1, j+1, perms[j][1], perms[j][0]);
               for (int j = i; j < count; j++) 
                  permprint.printPermutation(2, j+1, perms[j][2], null);
            }
         }
         permprint.close();
      } catch (Exception e) {
         if (Debug.debug) Debug.print(Debug.ERR, e);
         System.err.println(_("Failed to save permutations: ")+e.getMessage());
         syntax();
         System.exit(1);
      }
   }

   public static void curtains(HashMap/* <String,String> */ options, byte[][][] boards)
   {
      try {
         PrintStream curtainfile = new PrintStream(new FileOutputStream((String) options.get("--curtains")));
         Printer curtainprint = null;
         if ("txt".equals(options.get("--format"))) 
            curtainprint = new TextPrinter(curtainfile, options.get("--title")+" - "+_("Curtain Cards"));
         else if ("html".equals(options.get("--format")))
            curtainprint = new HTMLPrinter(curtainfile, options.get("--title")+" - "+_("Curtain Cards"));
         else if ("pdf".equals(options.get("--format")))
            curtainprint = new PDFPrinter(curtainfile, options.get("--title")+" - "+_("Curtain Cards"));
         else {
            System.err.println(_("Invalid format ")+options.get("--format"));
            System.exit(1);
         }
         int count = boards.length;
         int i = 0;
         while (i < count) {
            int j;
            for (j = i; j < i+6 && j < count; j++) {
               curtainprint.printCurtains(j+1, boards[j]);
            }
            for (j = i; j < i+6 && j < count; j++) {
               curtainprint.printCurtainBack(j+1);
            }
            i = j;
         }
         curtainprint.close();
      } catch (Exception e) {
         if (Debug.debug) Debug.print(Debug.ERR, e);
         System.err.println(_("Failed to save curtaincards: ")+e.getMessage());
         syntax();
         System.exit(1);
      }
   }

   public static boolean contains(byte[] hand, int card)
   {
      for (int i = 0; i < hand.length; i++) {
         if (Debug.debug) Debug.print(Debug.VERBOSE, "hand["+i+"]: "+hand[i]+", card: "+card);
         if (hand[i] == card) return true;
      }
      return false;
   }


   public static int length(byte[] hand, int suit)
   {
      int len = 0;
      if (suit < 10) {
         suit++;
         suit <<= 4;
      }
      for (int i = 0; i < hand.length; i++)
         if ((hand[i] & 0xf0) == suit) len++;
      return len;
   }

   public static boolean bacon(byte[] hand)
   {
      // singleton beer
      if (1 == length(hand, DIAMONDS) && 
            contains(hand, DIAMONDS | 7)) return true;

      // 4 of a kind
      for (int i = 2; i < ACE; i++)
         if (contains(hand, CLUBS | i) &&
             contains(hand, DIAMONDS | i) &&
             contains(hand, HEARTS | i) &&
             contains(hand, SPADES | i)) return true;

      // straight flush
      for (int i = 0, s = 0, c = 0, l = 0; i < hand.length; i++) {
         if ((hand[i] & 0x0f) == (c+1)) {
            l++;
            c++;
         } else {
             l = 0;
             c = hand[i] & 0x0f;
         }
         if ((hand[i] & 0xf0) != s) {
            l = 0;
            s = hand[i] & 0xf0;
         }
         if (l == 5) return true;
      }

      return false;
   }

	@SuppressWarnings("unchecked")
   public static void main(String[] args)
   {
      if (Debug.debug) {
         File f = new File("debug.conf");
         try {
            if (f.exists())
               Debug.loadConfig(f);
         } catch (Exception e) {}
         Debug.setThrowableTraces(true);
      }
      // read options
      HashMap/* <String,String> */ options = new HashMap/* <String,String> */();
      options.put("--help", null);
      options.put("--generate", null);
      options.put("--load", null);
      options.put("--load-dds", null);
      options.put("--load-analysis", null);
      options.put("--permutations", null);
      options.put("--demo", null);
      options.put("--save", null);
      options.put("--save-dds", null);
      options.put("--save-analysis", null);
      options.put("--output", null);
      options.put("--curtains", null);
      options.put("--stats", null);
      options.put("--analyze", null);
      options.put("--probability", null);
      options.put("--criteria", null);
      options.put("--format", "txt");
      options.put("--title", "Pescetti PseudoDuplimate");
      parseopts(options, args);

      byte[][][] boards = null;
      byte[][] tricks = null;

      // get boards somehow, or demo
      if (null != options.get("--help")) {
         syntax();
         System.exit(0);
      } else if (null != options.get("--generate")) {
         int count = 0;
         try {
            count = Integer.parseInt((String) options.get("--generate"));
         } catch (Exception e) {
            if (Debug.debug) Debug.print(Debug.ERR, e);
            System.err.println(_("Failed to parse number of boards to generate: ")+e.getMessage());
            syntax();
            System.exit(1);
         }

         boards = new byte[count][][];

         if (null != options.get("--criteria")) {
            HashMap<Criteria, Double> cs = null;
            try {
               cs = Criteria.parseCriteria((String) options.get("--criteria"));
            } catch (Exception e) {
               if (Debug.debug) Debug.print(Debug.ERR, e);
               System.err.println(_("Failed to parse criteria: ")+e.getMessage());
               syntax();
               System.exit(1);
            }

            BoardChecker bc = new BoardChecker(cs);
            double probability = 1.0;
            int maxattempts = 100000;
            if (null != options.get("--probability"))
            try {
               probability = Double.parseDouble((String) options.get("--probability"));
            } catch (Exception e) {
               if (Debug.debug) Debug.print(Debug.ERR, e);
               System.err.println(_("Failed to parse probability: ")+e.getMessage());
               syntax();
               System.exit(1);
            }
            Random r = new Random();
            for (int i = 0; i < count; i++) {
               double cw = ((double) r.nextInt(1073741824)) / 1073741824.0;
               boards[i] = permute(INITIAL_HAND, generatePermutation(false));
               if (Debug.debug) Debug.print(Debug.DEBUG, "board "+i+" probability factor "+probability+" current number "+cw);
               if (cw < probability) {
                  if (Debug.debug) {
                     for (int j = 0; j < 4; j++)
                        Debug.print(Debug.VERBOSE, "hand "+j+" has "+count(boards[i][j])+" points");
                     for (int p = 0; p < 4; p++)
                        for (int s = 0; s < 4; s++)
                           Debug.print(Debug.VERBOSE, "hand "+p+" suit "+s+" is "+length(boards[i][p], s));
                  }
                  boolean match = bc.check(boards[i]);
                  if (!match && maxattempts > 0) {
                     i--;
                     maxattempts--; // don't try forever to create matching hands
                     continue;
                  } else
                     if (Debug.debug) Debug.print(Debug.INFO, "Board "+i+" matches criteria");
               } 
            }
         } else
            for (int i = 0; i < count; i++)
               boards[i] = permute(INITIAL_HAND, generatePermutation(false));
      } else if (null != options.get("--load")) {
         try {
            InputStream in = new BufferedInputStream(new FileInputStream((String) options.get("--load")));
            Object[] tmp = combinedimport(in);
            boards = (byte[][][]) tmp[0];
            tricks = (byte[][]) tmp[1];
            in.close();
         } catch (Exception e) {
            if (Debug.debug) Debug.print(Debug.ERR, e);
            System.err.println(_("Failed to load boards: ")+e.getMessage());
            syntax();
            System.exit(1);
         }

      } else if (null != options.get("--load-dds")) {
         try {
            InputStream in = new BufferedInputStream(new FileInputStream((String) options.get("--load-dds")));
            boards = ddsimport(in);
            in.close();
         } catch (Exception e) {
            if (Debug.debug) Debug.print(Debug.ERR, e);
            System.err.println(_("Failed to load boards: ")+e.getMessage());
            syntax();
            System.exit(1);
         }
      } else if (null != options.get("--demo")) {

         demo();

      } else {
         System.out.println(_("Must specify either --generate --load{,-dds} or --demo"));
         syntax();
         System.exit(1);
      }

      if (null != options.get("--load-analysis")) {
         try {
            InputStream in = new BufferedInputStream(new FileInputStream((String) options.get("--load-analysis")));
            tricks = importanalysis(in);
            in.close();
         } catch (Exception e) {
            if (Debug.debug) Debug.print(Debug.ERR, e);
            System.err.println(_("Failed to load analysis: ")+e.getMessage());
            syntax();
            System.exit(1);
         }

      }

      // analyze if asked for
      if (null != options.get("--analyze")) {
         tricks = new byte[boards.length][];
         for (int i = 0; i < boards.length; i++) 
            tricks[i] = ddanalyse(boards[i]);
      }

      // export boards if asked for
      if (null != options.get("--save-dds") && null != boards) {
         String file = (String) options.get("--save-dds");
         try {
            PrintStream out = new PrintStream(new FileOutputStream(file));
            for (int i = 0; i < boards.length; i++)
               ddsexport(out, boards[i], false);
            out.close();
         } catch (Exception e) {
            if (Debug.debug) Debug.print(Debug.ERR, e);
            System.err.println(_("Failed to save boards: ")+e.getMessage());
            syntax();
            System.exit(1);
         }
      }

      // export boards if asked for
      if (null != options.get("--save") && null != boards) {
         String file = (String) options.get("--save");
         try {
            PrintStream out = new PrintStream(new FileOutputStream(file));
            combinedexport(out, boards, tricks);
            out.close();
         } catch (Exception e) {
            if (Debug.debug) Debug.print(Debug.ERR, e);
            System.err.println(_("Failed to save boards: ")+e.getMessage());
            syntax();
            System.exit(1);
         }
      }

      // export boards if asked for
      if (null != options.get("--save-analysis")) {
         try {
            if (null == tricks) throw new Exception(_("You must specify --load, --load-analysis or --analyze to save the analysis"));
            String file = (String) options.get("--save-analysis");
            PrintStream out = new PrintStream(new FileOutputStream(file));
            exportanalysis(out, tricks);
            out.close();
         } catch (Exception e) {
            if (Debug.debug) Debug.print(Debug.ERR, e);
            System.err.println(_("Failed to save analysis: ")+e.getMessage());
            syntax();
            System.exit(1);
         }
      }

      // generate permutations and save, if ased for
      if (null != options.get("--permutations") && null != boards) {
         permutation_genandsave(options, boards);
      }

      // save curtain cards, if ased for
      if (null != options.get("--curtains") && null != boards) {
         curtains(options, boards);
      }

      // redirect output if asked for
      PrintStream out = System.out;
      if (null != options.get("--output")) 
         try {
         out = new PrintStream(new FileOutputStream((String) options.get("--output")));
         } catch (Exception e) {
            if (Debug.debug) Debug.print(e);
            System.err.println(_("Cannot open file for output: ")+e.getMessage());
            System.exit(1);
         }
      Printer print = null;
      if ("txt".equals(options.get("--format"))) 
         print = new TextPrinter(out, (String) options.get("--title"));
      else if ("html".equals(options.get("--format")))
         print = new HTMLPrinter(out, (String) options.get("--title"));
      else if ("pdf".equals(options.get("--format")))
         print = new PDFPrinter(out, (String) options.get("--title"));
      else {
         System.err.println(_("Invalid format ")+options.get("--format"));
         System.exit(1);
      }

      // print boards and tricks if asked for
		byte[] points = new byte[4];
      for (int i = 0; i < boards.length; i++) {
         print.printHand(i+1, boards[i]);
			points[NORTH] = (byte) count(boards[i][NORTH]);
			points[SOUTH] = (byte) count(boards[i][SOUTH]);
			points[EAST] = (byte) count(boards[i][EAST]);
			points[WEST] = (byte) count(boards[i][WEST]);
			print.printPoints(points);
         if (null != tricks)
            print.printTricks(tricks[i]);
      }

      // print stats if asked for
      if (null != options.get("--stats")) {
         double[] stats = getStats(boards, tricks);
         print.printStats(stats);
      }
      print.close();
   }
}
