/*
 * Copyright (c) 1998-2020 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.write;

import com.google.common.collect.Iterables;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Formatter;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import ucar.ma2.DataType;
import ucar.ma2.ForbiddenConversionException;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.EnumTypedef;
import ucar.nc2.Group;
import ucar.nc2.NetcdfFile;
import ucar.nc2.NetcdfFiles;
import ucar.nc2.Structure;
import ucar.nc2.Variable;
import ucar.nc2.constants.CDM;
import ucar.nc2.iosp.netcdf4.Nc4;
import ucar.nc2.util.Indent;
import ucar.unidata.util.StringUtil2;

/**
 * Common Data Language (CDL) writer.
 * 
 * @see "https://www.unidata.ucar.edu/software/netcdf/docs/netcdf_utilities_guide.html#cdl_guide"
 */
public class CDLWriter {
  /**
   * Write CDL to a PrintStream.
   * 
   * @deprecated do not use
   */
  @Deprecated
  public static void writeCDL(NetcdfFile ncfile, PrintStream out, boolean strict) {
    Formatter f = new Formatter();
    CDLWriter.writeCDL(ncfile, f, strict, null);
    PrintWriter pw = new PrintWriter(out);
    pw.write(f.toString());
  }

  /**
   * Write CDL to a Writer (legacy)
   * 
   * @deprecated do not use
   */
  @Deprecated
  public static void writeCDL(NetcdfFile ncfile, Writer out, boolean strict) {
    Formatter f = new Formatter();
    CDLWriter.writeCDL(ncfile, f, strict, null);
    PrintWriter pw = new PrintWriter(out);
    pw.write(f.toString());
  }

  /** Write CDL to a Formatter */
  public static void writeCDL(NetcdfFile ncfile, Formatter out, boolean strict, @Nullable String nameOverride) {
    CDLWriter writer = new CDLWriter(ncfile, out, strict);
    writer.toStringStart(new Indent(2), strict, nameOverride);
    writer.toStringEnd();
  }

  private final NetcdfFile ncfile;
  private final Formatter out;
  private final boolean strict;

  CDLWriter(NetcdfFile ncfile, Formatter out, boolean strict) {
    this.ncfile = ncfile;
    this.out = out;
    this.strict = strict;
  }

  void toStringStart(Indent indent, boolean strict, @Nullable String nameOverride) {
    String name = (nameOverride != null) ? nameOverride : ncfile.getLocation();
    if (strict) {
      if (name.endsWith(".nc"))
        name = name.substring(0, name.length() - 3);
      if (name.endsWith(".cdl"))
        name = name.substring(0, name.length() - 4);
      name = NetcdfFiles.makeValidCDLName(name);
    }
    out.format("%snetcdf %s {%n", indent, name);
    indent.incr();
    writeCDL(ncfile.getRootGroup(), indent);
    indent.decr();
  }

  void toStringEnd() {
    out.format("}%n");
  }

  private void writeCDL(Group group, Indent indent) {
    List<EnumTypedef> enumTypedefs = group.getEnumTypedefs();
    boolean hasE = (!enumTypedefs.isEmpty());
    boolean hasD = (!group.getDimensions().isEmpty());
    boolean hasV = (!group.getVariables().isEmpty());
    // boolean hasG = (groups.size() > 0);
    boolean hasA = (!Iterables.isEmpty(group.attributes()));

    if (hasE) {
      out.format("%stypes:%n", indent);
      indent.incr();
      for (EnumTypedef e : enumTypedefs) {
        writeCDL(e, indent);
        out.format("%n");
      }
      indent.decr();
      out.format("%n");
    }

    if (hasD) {
      out.format("%sdimensions:%n", indent);
      indent.incr();
      for (Dimension myd : group.getDimensions()) {
        writeCDL(myd, indent);
        out.format("%n");
      }
      indent.decr();
    }

    if (hasV) {
      out.format("%svariables:%n", indent);
      indent.incr();
      for (Variable v : group.getVariables()) {
        if (v instanceof Structure)
          writeCDL((Structure) v, indent, false);
        else
          writeCDL(v, indent, false);
        out.format("%n");
      }
      indent.decr();
    }

    for (Group g : group.getGroups()) {
      String gname = strict ? NetcdfFiles.makeValidCDLName(g.getShortName()) : g.getShortName();
      out.format("%sgroup: %s {%n", indent, gname);
      indent.incr();
      writeCDL(g, indent);
      indent.decr();
      out.format("%s}%n%n", indent);
    }

    // if (hasA && (hasE || hasD || hasV || hasG))
    // out.format("%n");

    if (hasA) {
      if (group.isRoot())
        out.format("%s// global attributes:%n", indent);
      else
        out.format("%s// group attributes:%n", indent);

      for (Attribute att : group.attributes()) {
        // String name = strict ? NetcdfFile.escapeNameCDL(getShortName()) : getShortName();
        if (!CDM.isspecial(att)) {
          out.format("%s", indent);
          writeCDL(att, null);
          out.format(";");
          if (!strict && (att.getDataType() != DataType.STRING))
            out.format(" // %s", att.getDataType());
          out.format("%n");
        }
      }
    }
  }

  private void writeCDL(Attribute att, String parentname) {
    if (strict && (att.isString() || att.getEnumType() != null)) {
      // Force type explicitly for string.
      out.format("string "); // note lower case and trailing blank
    }
    if (strict && parentname != null) {
      out.format(NetcdfFiles.makeValidCDLName(parentname));
    }
    out.format(":");
    out.format("%s", strict ? NetcdfFiles.makeValidCDLName(att.getShortName()) : att.getShortName());
    if (att.isString()) {
      out.format(" = ");
      for (int i = 0; i < att.getLength(); i++) {
        if (i != 0) {
          out.format(", ");
        }
        String val = att.getStringValue(i);
        if (val != null) {
          out.format("\"%s\"", encodeString(val));
        }
      }
    } else if (att.getEnumType() != null) {
      out.format(" = ");
      for (int i = 0; i < att.getLength(); i++) {
        if (i != 0) {
          out.format(", ");
        }
        EnumTypedef en = att.getEnumType();
        String econst = att.getStringValue(i);
        Integer ecint = en.lookupEnumInt(econst);
        if (ecint == null) {
          throw new ForbiddenConversionException("Illegal enum constant: " + econst);
        }
        out.format("\"%s\"", encodeString(econst));
      }
    } else {
      out.format(" = ");
      for (int i = 0; i < att.getLength(); i++) {
        if (i != 0)
          out.format(", ");

        DataType dataType = att.getDataType();
        Number number = att.getNumericValue(i);
        if (dataType.isUnsigned()) {
          // 'number' is unsigned, but will be treated as signed when we print it below, because Java only has signed
          // types. If it is large enough ( >= 2^(BIT_WIDTH-1) ), its most-significant bit will be interpreted as the
          // sign bit, which will result in an invalid (negative) value being printed. To prevent that, we're going
          // to widen the number before printing it.
          number = DataType.widenNumber(number);
        }
        out.format("%s", number);

        if (dataType.isUnsigned()) {
          out.format("U");
        }

        if (dataType == DataType.FLOAT)
          out.format("f");
        else if (dataType == DataType.SHORT || dataType == DataType.USHORT) {
          out.format("S");
        } else if (dataType == DataType.BYTE || dataType == DataType.UBYTE) {
          out.format("B");
        } else if (dataType == DataType.LONG || dataType == DataType.ULONG) {
          out.format("L");
        }
      }
    }
  }

  private void writeCDL(Dimension dim, Indent indent) {
    String name = strict ? NetcdfFiles.makeValidCDLName(dim.getShortName()) : dim.getShortName();
    out.format("%s%s", indent, name);
    if (dim.isUnlimited())
      out.format(" = UNLIMITED;   // (%d currently)", dim.getLength());
    else if (dim.isVariableLength())
      out.format(" = UNKNOWN;");
    else
      out.format(" = %d;", dim.getLength());
  }

  private void writeCDL(EnumTypedef e, Indent indent) {
    String name = strict ? NetcdfFiles.makeValidCDLName(e.getShortName()) : e.getShortName();
    String basetype = "";
    switch (e.getBaseType()) {
      case ENUM1:
        basetype = "byte ";
        break;
      case ENUM2:
        basetype = "short ";
        break;
      case ENUM4:
        basetype = "";
        break;
      default:
        throw new IllegalStateException("Unexpected basetype = " + e.getBaseType());
    }
    out.format("%s%senum %s { ", indent, basetype, name);
    int count = 0;
    Map<Integer, String> map = e.getMap();
    List<Integer> keysetList = new ArrayList<>(map.keySet());
    Collections.sort(keysetList);
    for (Integer key : keysetList) {
      String s = map.get(key);
      if (0 < count++)
        out.format(", ");
      if (strict)
        out.format("%s = %s", NetcdfFile.makeValidCDLName(s), key);
      else
        out.format("'%s' = %s", s, key);
    }
    out.format("};");
  }

  private void writeCDL(Variable v, Indent indent, boolean useFullName) {
    out.format("%s", indent);
    DataType dataType = v.getDataType();
    if (dataType == null)
      out.format("Unknown");
    else if (dataType.isEnum()) {
      if (v.getEnumTypedef() == null)
        out.format("enum UNKNOWN");
      else
        out.format("enum %s", NetcdfFile.makeValidCDLName(v.getEnumTypedef().getShortName()));
    } else {
      String printname = dataType.toString();
      if (strict)
        printname = printname.toLowerCase();
      out.format("%s", printname);
    }

    // if (isVariableLength) out.append("(*)"); // LOOK
    out.format(" ");
    v.getNameAndDimensions(out, useFullName, strict);
    out.format(";");
    out.format("%n");

    indent.incr();
    for (Attribute att : v.attributes()) {
      if (CDM.isspecial(att))
        continue;
      out.format("%s", indent);
      writeCDL(att, v.getShortName());
      out.format(";");
      if (!strict && (att.getDataType() != DataType.STRING))
        out.format(" // %s", att.getDataType());
      out.format("%n");
    }
    indent.decr();
  }

  private static char[] org = {'\b', '\f', '\n', '\r', '\t', '\\', '\'', '\"'};
  private static String[] replace = {"\\b", "\\f", "\\n", "\\r", "\\t", "\\\\", "\\\'", "\\\""};

  /**
   * Replace special characters '\t', '\n', '\f', '\r', for CDL
   *
   * @param s string to quote
   * @return equivilent string replacing special chars
   */
  private static String encodeString(String s) {
    return StringUtil2.replace(s, org, replace);
  }

  private void writeCDL(Structure s, Indent indent, boolean useFullName) {
    out.format("%n%s%s {%n", indent, s.getDataType());

    indent.incr();
    for (Variable v : s.getVariables()) {
      // nested structures
      if (v instanceof Structure) {
        writeCDL((Structure) v, indent, useFullName);
      } else {
        writeCDL(v, indent, useFullName);
      }
    }
    indent.decr();

    out.format("%s} ", indent);
    s.getNameAndDimensions(out, useFullName, strict);
    out.format(";%n");

    for (Attribute att : s.attributes()) {
      if (CDM.isspecial(att))
        continue;
      out.format("%s", indent);
      writeCDL(att, s.getShortName());
      out.format(";");
      if (!strict && (att.getDataType() != DataType.STRING))
        out.format(" // %s", att.getDataType());
      out.format("%n");
    }
    out.format("%n");
  }

}
