package yi.iu.processor.texture.laws;

import java.awt.*;
import yi.iu.processor.filter.Average;
import yi.iu.processor.channel.CYMaker;
import yi.util.config.*;
import yi.util.io.ReadArgu;
import yi.util.io.OutputResult;
import yi.util.io.*;
import yi.util.io.input.*;
import yi.util.io.output.*;
import yi.util.log.*;
import yi.ai.cluster.*;
import yi.iu.processor.resample.*;
import yi.stat.graph.Graph;
import yi.stat.hist.*;
import yi.iu.processor.morph.*;

/**
 * <P>
 * Class DPU_LawsRegion generates the texture compatialbe regions.  Laws texture
 * energy measures and K-Means (or distance based) clustering algorithm are used.
 * </P>
 *
 * <P>
 * The main functionility is implemented in geTextureRegions.  (See its
 * descriptions for detail.)  However, gerReusableTextureRegions should be used
 * so that the calculation results can be used.
 * </P>
 *
 * <P>
 * Example:
 * </P>
 *
 * <CODE><PRE>
 *     DPU_LawsRegion dp = new DPU_LawsRegion();<BR/>
 *     Clusters clusters = dp.getReusableTextureRegions(pixels, d.width, d.height);<BR/>
 *     int cluster_num = clusters.getClusterNum();    // resultant cluster number<BR/>
 *     int data_dim = clusters.getDataDimension();    // data vector dimension<BR/>
 *     int[] labels = clusters.getClusterMap();       // the data -> cluster map<BR/>
 *     double[][] means = clusters.getClusterMeans(); // resultant cluster means<BR/>
 * </PRE></CODE>
 */
public class DPU_LawsRegion implements Configurable, RstReusable{
  /**
   * Parameter for K-Mean Clustering algorithm when init means using possibility.
   * The initialized means can't be decided beforehand, so a maximum is set first.
   * It is also useful when using distance clustering
   * @see yi.ai.cluster.KMCluster
   */
  public int MAX_CLUSTER_NUM = 32;

  /**
   * The size threshold of a cluster.  If a cluster is too small, then it will
   * not be generated. (currently only used for OneDSplitCluster)
   */
  public double SIZE_THRESHOLD = 0.03;
//
//  /**
//   * Parameter for K-Mean Clustering algorithm when init means using possibility.
//   * BIN_NUM is how many bins on each dimension
//   * @see yi.ai.cluster.KMCluster
//   */
//  public int BIN_NUM = 4;
//
//  /**
//   * Parameter for K-Mean Clustering algorithm when init means using possibility
//   * This parameter should be considered with BIN_NUM.  For example, if the
//   * 9-D laws vectors are quantilized using BIN_NUM = 4 (each dimension), then
//   * the total number of bins is 4 ^ 9 = 262,144, the average number of pixels
//   * falling into a bin is 128 * 128 / 262,144 = 0.0625 for a 128 * 128 image.
//   * If USE_BIN_RATIO = 20, then the threshold = 0.0625 * 20 = 1.25 ~= 1.  If
//   * you want the inited means to be those bins with at least 20 pixels, then
//   * set USE_BIN_RATIO = 20 / 0.0625 = 320.
//   */
//  public int USE_BIN_RATIO = 1600;

  /**
   * Parameter for K-Mean Clustering algorithm when using random init means.
   */
  public int K = 6;

  /**
   * Parameter for K-Mean Clustering algorithm when want to try several times of
   * clustering using random init means.  KMEANS_TRY_TIMES is how many times to
   * try.
   */
  public int KMEANS_TRY_TIMES = 4;

  /**
   * K_MEANS_CLUSTERING is a constant used to config the clustering method
   * parameter, CLUSTERING_METHOD.
   */
  public final static int K_MEANS_CLUSTERING  = 1;

  /**
   * DISTANCE_CLUSTERING is a constant used to config the clustering method
   * parameter, CLUSTERING_METHOD.
   */
  public final static int DISTANCE_CLUSTERING = 2;

  public final static int BINARY_1D_CLUSTERING = 3;

  public final static int RM_BAD_CLUSTERING = 4;

  public final static int RM_BAD_GUIDED_INIT_CLUSTERING = 5;

  public final static int ONE_D_SPLIT_CLUSTERING = 6;
  /**
   * CLUSTERING_METHOD is the parameter to determine which clustering algorithm
   * will be used to cluster Laws texture energy measures.
   */
  public int CLUSTERING_METHOD = K_MEANS_CLUSTERING;

  public int    REUSE  = DIU.INPUT_SER;
  public int    OUTPUT = DOU.OUTPUT_SER;
  public int    LOG_LEVEL = 3;


  public DPU_LawsRegion() {
    Configuration.configParameters(this);
  }


  public String[] getParameterNames() {
    return new String[] {
      "CLUSTERING_METHOD",
      "MAX_CLUSTER_NUM",
      "K",
      "KMEANS_TRY_TIMES",
      "LOG_LEVEL",
      "REUSE",
      "OUTPUT",
      "DEMO_TEXTURE_ENERGY_MEASURES"
    };
  }

  int m_reuseFrom;
  Object reuse(int reuse_level) {
    m_reuseFrom = DIU.INPUT_NONE;
    if (reuse_level != DIU.INPUT_NONE) {
      try {
        Clusters rst = (Clusters)DIU.in(
            reuse_level,
            DOU.shortName(this.getClass().getName()),
            "Clusters",
            Class.forName("yi.ai.cluster.Clusters"));
        if (rst != null) m_reuseFrom = reuse_level;
        return rst;
      }
      catch (Exception e) {
        LogHandler.log(this, LOG_LEVEL, 1, "Can't Reuse DS_LawRegion_Clusters:\n" + e);
      }
    }
    return null;
  }

  void output(int output_level, Clusters ds){
    if (output_level != DOU.OUTPUT_NONE) {
      try {
        DOU.out(
            output_level & (~ m_reuseFrom),
            DOU.shortName(this.getClass().getName()),
            DOU.shortName(ds.getClass().getName()),
            ds);
      } catch (DataOutputException e) {
        LogHandler.log(this, LOG_LEVEL, 1, e.toString());
      }
    }
  }

  /**
   * Use distance clustering (DistanceCluster) to clustering the texture
   * energy measures.
   * @param vectors  the texture energy measures
   * @param data_dim the data dimension
   * @param max_cluster_num the maximum allowed number of clusters
   * @return the resultant clusters
   * @see DistanceCluster
   */
  Clusters distanceClustering(int[][] vectors, int data_dim,
                                int max_cluster_num) {
    // Use yi.ai.cluster.DistanceCluster to cluster
    DistanceCluster cluster = new DistanceCluster();
    Clusters clusters = cluster.clustering(vectors, data_dim, MAX_CLUSTER_NUM);
    return clusters;
  }

  /**
   * Use K-Means clustering (KMCluster) to clustering the texture
   * energy measures.
   * @param vectors  the texture energy measures
   * @param data_dim the data dimension
   * @param max_cluster_num the maximum allowed number of clusters
   * @return the resultant clusters
   * @see KMCluster
   */
  Clusters kmeansClustering(int[][] vectors, int data_dim,
                                int max_cluster_num) {
    int[] maximums = new int[]{1200, 400, 2000, 4000, 2500, 5000, 700, 1000, 800};
//    // normalized data for use in K-means with means initialized using possibility
//    int unit = 255;
//    normalize(vectors, data_dim, maximums, unit);

//    // Use Possibility to init K-Means
//    KMCluster cluster = new KMCluster(vectors, BIN_NUM, USE_BIN_RATIO);

    // Use random to init K-Means
    KMCluster cluster = new KMCluster(K, KMCluster.RANDOM, vectors, maximums);

//    Clusters clusters = cluster.clustering();
    // use n-tries and pick up the best
    Clusters clusters = cluster.clusteringWithPick(KMEANS_TRY_TIMES, true);
    return clusters;
  }

  int label_num = 0;
  /**
   * clustering a region of the image.  It goes though each measure, calculate
   * the score (normalized sum of variance) for splitting the histogram on it.
   * Pick up one with the smallest, split it, and then call recursively
   * @param gray the image
   * @param width image width
   * @param height image height
   * @param labels the region labels for each pixel (pixel->region_label mapping)
   * @param the_label the label of region to be clustered
   * @param rst the Laws texture energy measures
   * @return the labels after clustering
   */
  int[] clusteringOnSubRegion(int[] gray,
                                     int width, int height,
                                     int[] labels, int the_label,
                                     int[][][] rst, int max_cluster_num) {

    if (label_num >= max_cluster_num) return labels;
    String img_name = Configuration.getParameter("SOURCE_NAME");
    System.out.println();
    System.out.println("considering " + the_label);
    System.out.println();

    int size_threshold = width * height / 20;
    double min_normalized_sum_of_var = 1;
    OtsuThresholding best = null;
    int split_on = -1;
    int[] split_based = null;
    // get the score for each possible split
    for (int i=0; i<rst.length; i++) {
      int[] rst1D = yi.util.data.Conversion.from2Dto1D(rst[i], width, height);
      // construct the histogram
      int max = yi.stat.Util.maximum(rst1D);
      SimpleHistogram hist = new SimpleHistogram(0, max);
      for (int j=0; j<rst1D.length; j++) {
        if (labels[j] == the_label) hist.putToBin(rst1D[j]);
      }

//      hist.output(img_name + "." + the_label + "." + i + ".hist");

      OtsuThresholding otsu = new OtsuThresholding();
      otsu.thresholding(hist.getHistogram(), hist.getMinValue());
      if (otsu.getNumber1() < size_threshold || otsu.getNumber2() < size_threshold) {
        continue;
      }

      double score = otsu.getScore();
      System.out.println(String.valueOf(the_label) + "." + i + " : " + score);
//      System.out.println(String.valueOf(the_label) + "." + i);
//      System.out.println(otsu);
      if (min_normalized_sum_of_var > score) {
        min_normalized_sum_of_var = score;
        best = otsu;
        split_on =  i;
        split_based = rst1D;
      }
    }
    System.out.println();
    System.out.println("best split on " + split_on + ", score = " + min_normalized_sum_of_var);
    System.out.println();
    if (min_normalized_sum_of_var < 0.3) {
      System.out.println("splitting to " + label_num + " and " + (label_num + 1));
      System.out.println();
      int threshold = best.getThreshold();
      // split
      for (int j=0; j<labels.length; j++) {
        if (labels[j] == the_label) {
          if (split_based[j] < threshold) labels[j] = label_num;
          else labels[j] = label_num + 1;
        }
      }

      // demo the higher part
      int[] seg = new int[gray.length];
      int num1 = 0, num2 = 0;
      for (int j=0; j<labels.length; j++) {
        if (labels[j] == label_num) {
            seg[j] = gray[j];
            num1 ++;
        } else if (labels[j] == label_num + 1){
            seg[j] = 0xff000000;
            num2 ++;
        }
      }
      System.out.println("->" + num1 + " , " + num2);
      System.out.println("->" + best.getNumber1() + " , " + best.getNumber2());

      System.out.println();
//      OutputResult.outputGrayImage(img_name + '.' + the_label + ".bin.pgm", seg, width, height);

//      int[] label_img = yi.util.ImageUtil.makeNewImageFromLabel_back(labels);
//      OutputResult.outputGIFImage(img_name + "." + the_label + ".gif", label_img, width, height);
      int current_num = label_num;
      label_num += 2;

      clusteringOnSubRegion(gray, width, height, labels, current_num, rst, max_cluster_num);
      clusteringOnSubRegion(gray, width, height, labels, current_num + 1, rst, max_cluster_num);
    }
    return labels;
  }


  Clusters binary1DClustering(int[] gray, int width, int height, int[][][] rst, int max_cluser_num) {
    String img_name = Configuration.getParameter("SOURCE_NAME");
    int[] labels = new int[gray.length];
    label_num = 1;
    clusteringOnSubRegion(gray, width, height, labels, 0, rst, max_cluser_num);

//    int[] label_img = yi.util.ImageUtil.makeNewImageFromLabel_back(labels);
//    OutputResult.outputGIFImage(img_name + "." + "DPU_Lfinal" + ".gif", label_img, width, height);


//    ConnectComponent cc = new ConnectComponent();
//    int[] cc_label = cc.compactLabeling(labels, new Dimension(width, height), false);
//    for (int i=0; i<cc_label.length; i++) cc_label[i]++; // for background 0
//    Morph.filterSmallRegions(cc_label, cc.getMaxLabel() + 1, width * height / 100, 0);
//    yi.util.label.Label.sequential(cc_label, null, true);
//
//    int[] cc_label_img = yi.util.ImageUtil.makeNewImageFromLabel(cc_label);
//    OutputResult.outputGIFImage(img_name + "." + "filtered.final" + ".gif", cc_label_img, width, height);

    return null;
  }

  public double BAD_GROUP_THRESHOLD = 0.35;
  public int TO_MAKE_INIT_MEANS = 3;
  Clusters removeBadClustering(int[] gray, int width, int height, int[][][] rst) {
    int[][] energys = new int[rst.length][width*height];
    for (int i=0; i<energys.length; i++) {
      energys[i] = yi.util.data.Conversion.from2Dto1D(rst[i], width, height);
    }
    OtsuThresholding[] thresholdings = binaryThresholding(energys);

    for (int i=0; i<thresholdings.length; i++) {
      if (thresholdings[i].getScore() >= BAD_GROUP_THRESHOLD) {
        for (int j=0; j<energys[i].length; j++) energys[i][j] = 0;
      }
    }

    // shape the data
    int data_dim = rst.length;
    int[][] vectors = new int[height*width][data_dim];
    for (int i=0; i<thresholdings.length; i++) {
      for (int j=0; j<energys[i].length; j++) {
        vectors[j][i] = energys[i][j];
      }
    }


    if (CLUSTERING_METHOD == RM_BAD_GUIDED_INIT_CLUSTERING) {
      double[][] means = new double[thresholdings.length][];
      double[] score = new double[thresholdings.length];
      for (int i=0; i<thresholdings.length; i++) {
        score[i] = thresholdings[i].getScore();
        System.out.println(thresholdings[i]);
      }
      yi.util.io.OutputResult.print(System.out, score);
      int[] order = order(score, false);
      for (int i=0; i<thresholdings.length; i++) {
        if (score[i] < BAD_GROUP_THRESHOLD) {
          if (order[i] <= TO_MAKE_INIT_MEANS) {
            System.out.println("score = " + score[i]);
            means[i] = new double[2];
            means[i][0] = thresholdings[i].getMean1();
            means[i][1] = thresholdings[i].getMean2();
          }
          else {
            means[i] = new double[1];
            means[i][0] = thresholdings[i].getMean();
          }
        } else {
          means[i] = new double[1];
          means[i][0] = 0;
        }
      }

//      for (int i=0; i<means.length; i++) {
//        for (int j=0; j<means[i].length; j++) {
//          System.out.print(means[i][j]);
//          System.out.print("  ");
//        }
//        System.out.println();
//      }
      double[][] init_means = constructInitMeans(means);
//      yi.util.io.OutputResult.print(System.out, init_means, init_means[0].length, init_means.length);
      int[][] int_init_means = new int[init_means.length][];
      for (int i=0; i<init_means.length; i++) {
        int_init_means[i] = new int[init_means[i].length];
        for (int j=0; j<int_init_means[i].length; j++) {
          int_init_means[i][j] = (int)Math.round(init_means[i][j]);
        }
      }
      yi.util.io.OutputResult.print(System.out, int_init_means, int_init_means[0].length, int_init_means.length);

      KMCluster cluster = new KMCluster(int_init_means, vectors);

      Clusters clusters = cluster.clustering();
      return clusters;
    }
    else { // RM_BAD_CLUSTERING
      return kmeansClustering(vectors, data_dim, MAX_CLUSTER_NUM);
    }
  }

  double[][] constructInitMeans(double[][] means) {
    double[][] rst = new double[means[0].length][1];
    int current_num = means[0].length, current_len = 1;
    for (int i=0; i<means[0].length; i++) rst[i][0] = means[0][i];
    for (int i=1; i<means.length; i++) {
      double[][] current_rst = new double[current_num * means[i].length][current_len + 1];
      for (int j=0; j<means[i].length; j++) {
        for (int k=0; k<current_num; k++) {
          System.arraycopy(rst[k], 0, current_rst[j * current_num + k], 0, current_len);
          current_rst[j * current_num + k][current_len] = means[i][j];
        }
      }
      current_num = current_rst.length; current_len ++;
      rst = current_rst;
    }
    return rst;
  }

  public static class TestConstructInitMeans {
    public static void main(String[] args) {
      double[][] data = new double[][] {
        {1, 2},
        {3},
        {4,5,6},
        {7,8}
      };
      DPU_LawsRegion dp = new DPU_LawsRegion();
      double[][] rst = dp.constructInitMeans(data);
      yi.util.io.OutputResult.print(System.out, rst, data.length, rst.length);

    }
  }

  /**
   * The order of the highest one is 1
   * @param score
   * @param highIsBetter
   * @return
   */
  private int[] order(double[] score, boolean highIsBetter) {
    int[] order = new int[score.length];
    for (int i=0; i<score.length; i++) {
      int rank = 1;
      for (int j=0; j<score.length; j++) {
        if (score[j] > score[i]) rank++;
      }
      order[i] = rank;
    }
    if (!highIsBetter) {
      for (int i=0; i<score.length; i++) {
        order[i] = score.length - order[i] + 1;
      }
    }
    yi.util.io.OutputResult.print(System.out, order);
    return order;
  }

  OtsuThresholding[] binaryThresholding(int[][] energys) {
    OtsuThresholding[] thresholdings = new OtsuThresholding[energys.length];
    // get the score for each possible split
    for (int i=0; i<energys.length; i++) {
      int[] rst1D = energys[i];
      // construct the histogram
      SimpleHistogram hist = new SimpleHistogram(rst1D);

      hist.output("hist" + i);

      thresholdings[i] = new OtsuThresholding();
      thresholdings[i].thresholding(hist.getHistogram(), hist.getMinValue());

//      System.out.print("threshold = " + (thresholdings[i].getThreshold() + hist.getMinValue()));
//      System.out.println(", score = " + thresholdings[i].getScore());

    }
    return thresholdings;
  }

  /**
   * getTextureRegions return texture compatible regions of the given image.  It
   * first get Law Texture Energy Measures (calling DPU_Laws), then use one
   * clustering algorithm to cluster the Law Texture Energy Measure Vectors.
   * The means of resultant clusters are returned.  The possible clustering
   * algorithms are kmeansClutering and distanceClustering.  Which one to use in
   * is determined by the parameter CLUSTERING_METHOD.
   * @param grays  the image pixel values in gray levels
   * @param width  the image width
   * @param height the image height
   * @return       the resultant clusters
   * @see DPU_Laws
   */
  public Clusters getTextureRegions(int[] grays, int width, int height) {
    // change 1-D grays to 2-D first to call Laws
    int[][] pixels2D = yi.util.ImageUtil.oneD2twoD(grays, new Dimension(width, height));  // change to 2D
    DPU_Laws dp = new DPU_Laws();
    // get laws
    int[][][] rst = dp.getTextureEnergyMeasures(pixels2D, width, height);

    demoTextureEnergyMeasures(rst, width, height);


    if (CLUSTERING_METHOD == BINARY_1D_CLUSTERING) {
      return binary1DClustering(grays, width, height, rst, MAX_CLUSTER_NUM);
    }

    if (CLUSTERING_METHOD == RM_BAD_CLUSTERING ||
        CLUSTERING_METHOD == RM_BAD_GUIDED_INIT_CLUSTERING) {
      return removeBadClustering(grays, width, height, rst);
    }

    // shape the data
    int data_dim = rst.length;

    int[][] vectors = new int[height*width][data_dim];
    for (int h=0; h<height;h++) {
      for (int w=0; w<width; w++) {
        int index = h * width + w;
        for (int i=0; i<data_dim; i++) {
          vectors[index][i] = rst[i][h][w];
        }
      }
    }
    rst = null; // release the memory

    if (CLUSTERING_METHOD == K_MEANS_CLUSTERING) {
      return kmeansClustering(vectors, data_dim, MAX_CLUSTER_NUM);
    } else if (CLUSTERING_METHOD == DISTANCE_CLUSTERING) {
      return distanceClustering(vectors, data_dim, MAX_CLUSTER_NUM);
    } else if (CLUSTERING_METHOD == ONE_D_SPLIT_CLUSTERING) {
      OneDSplitCluster clustering = new OneDSplitCluster();
      return clustering.clustering(vectors, data_dim, MAX_CLUSTER_NUM, SIZE_THRESHOLD);
    } else {
      System.err.println("the clustering method specified in CLUSTERING_METHOD is not supported");
      return null;
    }
  }

  public boolean DEMO_TEXTURE_ENERGY_MEASURES = false;
  private void demoTextureEnergyMeasures(int[][][] rst, int width, int height) {
    if (DEMO_TEXTURE_ENERGY_MEASURES) {
      for (int i=0; i<rst.length; i++) {
        int[] graph = yi.stat.graph.Graph.grayGraph(
                        yi.util.data.Conversion.from2Dto1D(rst[i], width, height)
                      );
        String filename = Configuration.getStringParameter("SOURCE_NAME", "img") + ".energy." + i;
        yi.util.io.OutputResult.outputColorImage(filename + ".pgm", graph, width, height);
        yi.util.io.OutputResult.print(filename + ".rst" , rst[i], width, height);
      }
    }
  }

  /**
   * Normalize vectors.  For example, a vector (500, 25, 750) will be normalized
   * to (128, 64, 192) if the maximums on the dimensions are (1000, 100, 1000)
   * and the unit is 256.
   * @param vectors  the vectors to be normalized
   * @param data_dim the dimension number of the vectors
   * @param maximums the maximums on each dimension
   * @param unit     the unit "1" of the normalization
   */
  void normalize(int[][] vectors, int data_dim, int[] maximums, int unit) {
    for (int i=0; i<vectors.length; i++) {
//      for (int j=0; j<data_dim; j++) {
//        System.out.print(" " + vectors[i][j]);
//      }
//      System.out.println();
      for (int j=0; j<data_dim; j++) {
        if (vectors[i][j] > maximums[j]) vectors[i][j] = unit;
        else vectors[i][j] = vectors[i][j] * unit / maximums[j];
      }
//      for (int j=0; j<data_dim; j++) {
//        System.out.print(" " + vectors[i][j]);
//      }
//      System.out.println();
    }
  }

  /**
   * getReusableTextureRegions has all the functionalities getTextureRegions has,
   * while it adds the reusable functionalities
   * @param grays  the image pixel values in gray levels
   * @param width  the image width
   * @param height the image height
   * @return       the resultant clusters
   * @see getTextureRegions
   */
  public Clusters getReusableTextureRegions(int[] grays, int width, int height) {
      Clusters rst = (Clusters)reuse(REUSE);

      if (rst == null) {
        rst = getTextureRegions(grays, width, height);
      }

      output(OUTPUT, rst);

      return rst;
  }

  public static void main(String[] args) {
    // init configuration
    Configuration.init(args);

    // get the image name
    String img_name = ReadArgu.getOption('i', args);
    if (img_name == null) {
      System.err.println("Usage: -i image_name");
      System.exit(1);
    }

    // read in image
    Image image = null;
    Dimension d = new Dimension();
    int[] pixels = null;
    try {
      image = yi.util.io.ImageReader.readImageFile(img_name);
      pixels = yi.util.io.ImageReader.getPixels(image, d);
    }
    catch (java.io.IOException e) {
      System.err.println("Can't read image " + img_name);
      e.printStackTrace();
      System.exit(2);
    }

//    // scale down the image first
//    Dimension new_d = Resampler.getDestSize(d.width, d.height, 160, 160);
//    Resampler resampler = new BSplineResampler(new_d.width, new_d.height);
//    pixels = resampler.getProcessed(pixels, d);
//    d = new_d;
//    // change SOURCE_NAME to distinguish the scaled down image
//    img_name += ".scaled_down_160";

    // process
    // change to 2D gray level image first
    pixels = CYMaker.getLuminance(pixels, d);               // change to gray
    Configuration.setParameter("SOURCE_NAME", img_name);

    // just for test, these values may be used by demonstration.
    System.setProperty("img_width", String.valueOf(d.width));
    System.setProperty("img_height", String.valueOf(d.height));


    DPU_LawsRegion dp = new DPU_LawsRegion();
    Clusters clusters = dp.getReusableTextureRegions(pixels, d.width, d.height);
    int cluster_num = clusters.getClusterNum();
    int data_dim = clusters.getDataDimension();
    int[] labels = clusters.getClusterMap();
    double[][] means = clusters.getClusterMeans();

    int[] rst_img = yi.util.ImageUtil.makeNewImageFromLabel_back(labels);

    OutputResult.outputGIFImage(img_name + "." + "DPU_LawsRegion" + ".gif", rst_img, d.width, d.height);

//    System.out.println();
//    System.out.println("clusters:");
//    for (int i=0; i<cluster_num; i++) {
//      System.out.println("c" + i + " ");
//      for (int j=0; j<data_dim; j++) {
//        System.out.print(" " + means[i][j]);
//      }
//      System.out.println();
//    }
//
    System.exit(0);
  }

}