content: /*************************************************************************
  *                                                                        *
  *  This file is part of the 20n/act project.                             *
  *  20n/act enables DNA prediction for synthetic biology/bioengineering.  *
  *  Copyright (C) 2017 20n Labs, Inc.                                     *
  *                                                                        *
  *  Please direct all queries to act@20n.com.                             *
  *                                                                        *
  *  This program is free software: you can redistribute it and/or modify  *
  *  it under the terms of the GNU General Public License as published by  *
  *  the Free Software Foundation, either version 3 of the License, or     *
  *  (at your option) any later version.                                   *
  *                                                                        *
  *  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, see <http://www.gnu.org/licenses/>. *
  *                                                                        *
  *************************************************************************/
  
  package com.act.lcms.db.io.writer;
  
  import com.act.lcms.db.io.DB;
  import com.act.lcms.db.model.DeliveredStrainWell;
  import com.act.lcms.db.model.InductionWell;
  import com.act.lcms.db.model.LCMSWell;
  import com.act.lcms.db.model.Plate;
  import com.act.lcms.db.model.PregrowthWell;
  import com.act.lcms.db.model.StandardWell;
  import org.apache.commons.lang3.StringUtils;
  
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.io.Writer;
  import java.util.Arrays;
  import java.util.List;
  
  public class PlateCompositionWriter {
    public static final String PLATE_TYPE_96_WELLS = "plate_96_well";
    public static final String PLATE_TYPE_96_WELLS_BLOCK = "block_96_well";
    public static final String PLATE_TYPE_24_WELLS = "plate_24_well";
    public static final String PLATE_TYPE_24_WELLS_BLOCK = "block_24_well";
  
    public static final Integer PLATE_DIM_96_WELL_X = 12;
    public static final Integer PLATE_DIM_96_WELL_Y = 8;
    public static final Integer PLATE_DIM_24_WELL_X = 6;
    public static final Integer PLATE_DIM_24_WELL_Y = 4;
  
    public static final List<String> PLATE_LABELS_SAMPLE_WELLS = Arrays.asList("msid", "composition", "chemical", "note");
    public static final List<String> PLATE_LABELS_STANDARD_WELLS = Arrays.asList("chemical", "media");
    public static final List<String> PLATE_LABELS_INDUCTION_WELLS =
        Arrays.asList("msid", "chemical_source", "composition", "chemical", "strain_source", "note", "growth");
    public static final List<String> PLATE_LABELS_PREGROWTH_WELLS =
        Arrays.asList("source_plate", "msid", "composition", "source_well", "note", "growth");
  
  
    public void writeLCMSWellComposition(PrintWriter w, Plate plate, List<LCMSWell> wells) throws IOException {
      switch (plate.getPlateType()) {
        case PLATE_TYPE_96_WELLS:
        case PLATE_TYPE_96_WELLS_BLOCK:
          String[][][] tables = new String[4][PLATE_DIM_96_WELL_Y][PLATE_DIM_96_WELL_X];
          for (LCMSWell well : wells) {
            int y = well.getPlateRow();
            int x = well.getPlateColumn();
            // TODO: should we use a map for this instead to make the table name -> data correspondence clearer?
            tables[0][y][x] = well.getMsid();
            tables[1][y][x] = well.getComposition();
            tables[2][y][x] = well.getChemical();
            tables[3][y][x] = well.getNote();
          }
  
          for (int i = 0; i < PLATE_LABELS_SAMPLE_WELLS.size(); i++) {
            write96CellTable(w, PLATE_LABELS_SAMPLE_WELLS.get(i), tables[i]);
          }
          break;
        default:
          // TODO: use a better exception type here.
          throw new RuntimeException(String.format("Found unexpected plate type: %s", plate.getPlateType()));
      }
    }
  
    public void writeStandardWellComposition(PrintWriter w, Plate plate, List<StandardWell> wells)
        throws IOException {
      switch (plate.getPlateType()) {
        case PLATE_TYPE_96_WELLS:
        case PLATE_TYPE_96_WELLS_BLOCK:
          String[][][] tables = new String[2][PLATE_DIM_96_WELL_Y][PLATE_DIM_96_WELL_X];
          for (StandardWell well : wells) {
            int y = well.getPlateRow();
            int x = well.getPlateColumn();
            // TODO: should we use a map for this instead to make the table name -> data correspondence clearer?
            tables[0][y][x] = well.getChemical();
            tables[1][y][x] = well.getMedia();
            // TODO: add notes when we expect them.
          }
  
          for (int i = 0; i < PLATE_LABELS_STANDARD_WELLS.size(); i++) {
            write96CellTable(w, PLATE_LABELS_STANDARD_WELLS.get(i), tables[i]);
          }
          break;
        default:
          // TODO: use a better exception type here.
          throw new RuntimeException(String.format("Found unexpected plate type: %s", plate.getPlateType()));
      }
    }
  
    public void writeDeliveredStrainWellComposition(PrintWriter w, Plate plate, List<DeliveredStrainWell> wells)
        throws IOException {
      switch (plate.getPlateType()) {
        case PLATE_TYPE_96_WELLS:
        case PLATE_TYPE_96_WELLS_BLOCK:
          // Strain files don't use normal plate tables.
          w.println(">>wells\tmsid\tcomposition");
          for (DeliveredStrainWell well : wells) {
            w.println(StringUtils.join(new String[] {
                well.getWell(), well.getMsid(), well.getComposition()
            }, "\t"));
          }
          w.println();
          break;
        default:
          // TODO: use a better exception type here.
          throw new RuntimeException(String.format("Found unexpected plate type: %s", plate.getPlateType()));
      }
    }
  
    public void writeInductionWellComposition(PrintWriter w, Plate plate, List<InductionWell> wells)
        throws IOException {
      switch (plate.getPlateType()) {
        case PLATE_TYPE_24_WELLS:
        case PLATE_TYPE_24_WELLS_BLOCK:
          String[][][] tables = new String[7][PLATE_DIM_24_WELL_Y][PLATE_DIM_24_WELL_X];
          for (InductionWell well : wells) {
            int y = well.getPlateRow();
            int x = well.getPlateColumn();
            tables[0][y][x] = well.getMsid();
            tables[1][y][x] = well.getChemicalSource();
            tables[2][y][x] = well.getComposition();
            tables[3][y][x] = well.getChemical();
            tables[4][y][x] = well.getStrainSource();
            tables[5][y][x] = well.getNote();
            tables[6][y][x] = well.getGrowth() == null ? "" : well.getGrowth().toString();
          }
  
          for (int i = 0; i < PLATE_LABELS_INDUCTION_WELLS.size(); i++) {
            write24CellTable(w, PLATE_LABELS_INDUCTION_WELLS.get(i), tables[i]);
          }
          break;
        default:
          // TODO: use a better exception type here.
          throw new RuntimeException(String.format("Found unexpected plate type: %s", plate.getPlateType()));
      }
    }
  
    public void writePregrowthWellComposition(PrintWriter w, Plate plate, List<PregrowthWell> wells)
        throws IOException {
      switch (plate.getPlateType()) {
        case PLATE_TYPE_24_WELLS:
        case PLATE_TYPE_24_WELLS_BLOCK:
          String[][][] tables = new String[6][PLATE_DIM_24_WELL_Y][PLATE_DIM_24_WELL_X];
          for (PregrowthWell well : wells) {
            int y = well.getPlateRow();
            int x = well.getPlateColumn();
            tables[0][y][x] = well.getSourcePlate();
            tables[1][y][x] = well.getMsid();
            tables[2][y][x] = well.getComposition();
            tables[3][y][x] = well.getSourceWell();
            tables[4][y][x] = well.getNote();
            tables[5][y][x] = well.getGrowth() == null ? "" : well.getGrowth().toString();
          }
  
          for (int i = 0; i < PLATE_LABELS_PREGROWTH_WELLS.size(); i++) {
            write24CellTable(w, PLATE_LABELS_PREGROWTH_WELLS.get(i), tables[i]);
          }
          break;
        default:
          // TODO: use a better exception type here.
          throw new RuntimeException(String.format("Found unexpected plate type: %s", plate.getPlateType()));
      }
    }
  
    protected void writePlateAttributes(PrintWriter w, Plate plate) throws IOException {
      w.format(">%s\t%s\n", "name", plate.getName());
      w.format(">%s\t%s\n", "description", plate.getDescription());
      w.format(">%s\t%s\n", "barcode", plate.getBarcode());
      w.println(">schema\tplate\n");
      w.format(">%s\t%s\n", "location", plate.getLocation());
      w.format(">%s\t%s\n", "plate_type", plate.getPlateType());
      if (plate.getSolvent() != null) {
        w.format(">%s\t%s\n", "solvent", plate.getSolvent());
      }
      w.format(">%s\t%d\n", "temperature", plate.getTemperature());
      w.println();
    }
  
    protected void write96CellTable(PrintWriter w, String tableName, String[][] values) {
      writeCellTable(w, tableName, values, PLATE_DIM_96_WELL_X, PLATE_DIM_96_WELL_Y);
    }
  
    protected void write24CellTable(PrintWriter w, String tableName, String[][] values) {
      writeCellTable(w, tableName, values, PLATE_DIM_24_WELL_X, PLATE_DIM_24_WELL_Y);
    }
  
    protected void writeCellTable(PrintWriter w, String tableName, String[][] values,
                                  int expectedXDim, int expectedYDim) {
      StringBuilder headerBuilder = new StringBuilder(String.format(">>%s", tableName));
      for (int i = 0; i < expectedXDim; i++) {
        // Columns are one-indexed in composition tables.
        headerBuilder.append(String.format("\t%d", i + 1));
      }
      w.println(headerBuilder.toString());
  
      if (values.length != expectedYDim) {
        throw new RuntimeException(String.format(
            "ERROR: found incorrect row dimension for 96 well plate.  Expected %d, but got %d.\n",
            expectedYDim, values.length));
      }
  
      char rowName = 'A';
      for (int i = 0; i < values.length; i++) {
        if (values[i].length != expectedXDim) {
          throw new RuntimeException(String.format(
              "ERROR: found incorrect column dimension for 96 well plate.  Expected %d, but got %d.\n",
              expectedXDim, values.length));
        }
        w.format("%c\t%s\n", rowName, StringUtils.join(values[i], '\t'));
        rowName++;
      }
      w.println();
    }
  
    public void writePlateCompositionByBarcode(DB db, String plateBarcode, Writer dest) throws Exception {
      Plate p = Plate.getPlateByBarcode(db, plateBarcode);
      writePlateComposition(db, p, dest);
    }
  
    public void writePlateCompositionByName(DB db, String name, Writer dest) throws Exception {
      Plate p = Plate.getPlateByName(db, name);
      writePlateComposition(db, p, dest);
    }
  
    public void writePlateComposition(DB db, Plate p, Writer dest) throws Exception {
      PlateCompositionWriter writer = new PlateCompositionWriter();
      PrintWriter w = new PrintWriter(dest, true);
      writePlateAttributes(w, p);
  
      switch (p.getContentType()) {
        case LCMS:
          List<LCMSWell> LCMSWells = LCMSWell.getInstance().getByPlateId(db, p.getId());
          writer.writeLCMSWellComposition(w, p, LCMSWells);
          break;
        case STANDARD:
          List<StandardWell> stdWells = StandardWell.getInstance().getByPlateId(db, p.getId());
          writer.writeStandardWellComposition(w, p, stdWells);
          break;
        case DELIVERED_STRAIN:
          List<DeliveredStrainWell> strainWells = DeliveredStrainWell.getInstance().getByPlateId(db, p.getId());
          writer.writeDeliveredStrainWellComposition(w, p, strainWells);
          break;
        case INDUCTION:
          List<InductionWell> inductionWells = InductionWell.getInstance().getByPlateId(db, p.getId());
          writer.writeInductionWellComposition(w, p, inductionWells);
          break;
        case PREGROWTH:
          List<PregrowthWell> pregrowthWells = PregrowthWell.getInstance().getByPlateId(db, p.getId());
          writer.writePregrowthWellComposition(w, p, pregrowthWells);
          break;
        default:
          throw new RuntimeException(String.format("Unrecognized table type: %s", p.getContentType()));
      }
    }
  }
  