content: /*
   * Copyright 2002-2019 Drew Noakes and contributors
   *
   *    Licensed under the Apache License, Version 2.0 (the "License");
   *    you may not use this file except in compliance with the License.
   *    You may obtain a copy of the License at
   *
   *        https://www.apache.org/licenses/LICENSE-2.0
   *
   *    Unless required by applicable law or agreed to in writing, software
   *    distributed under the License is distributed on an "AS IS" BASIS,
   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   *    See the License for the specific language governing permissions and
   *    limitations under the License.
   *
   * More information about this project is available at:
   *
   *    https://drewnoakes.com/code/exif/
   *    https://github.com/drewnoakes/metadata-extractor
   */
  
  package com.drew.metadata.icc;
  
  import com.drew.lang.BufferBoundsException;
  import com.drew.lang.ByteArrayReader;
  import com.drew.lang.RandomAccessReader;
  import com.drew.lang.annotations.NotNull;
  import com.drew.lang.annotations.Nullable;
  import com.drew.metadata.TagDescriptor;
  
  import java.io.IOException;
  import java.io.UnsupportedEncodingException;
  import java.text.DecimalFormat;
  
  import static com.drew.metadata.icc.IccDirectory.*;
  
  /**
   * @author Yuri Binev
   * @author Drew Noakes https://drewnoakes.com
   */
  @SuppressWarnings("WeakerAccess")
  public class IccDescriptor extends TagDescriptor<IccDirectory>
  {
      public IccDescriptor(@NotNull IccDirectory directory)
      {
          super(directory);
      }
  
      @Override
      public String getDescription(int tagType)
      {
          switch (tagType) {
              case TAG_PROFILE_VERSION:
                  return getProfileVersionDescription();
              case TAG_PROFILE_CLASS:
                  return getProfileClassDescription();
              case TAG_PLATFORM:
                  return getPlatformDescription();
              case TAG_RENDERING_INTENT:
                  return getRenderingIntentDescription();
          }
  
          if (tagType > 0x20202020 && tagType < 0x7a7a7a7a)
              return getTagDataString(tagType);
  
          return super.getDescription(tagType);
      }
  
      private static final int ICC_TAG_TYPE_TEXT = 0x74657874;
      private static final int ICC_TAG_TYPE_DESC = 0x64657363;
      private static final int ICC_TAG_TYPE_SIG = 0x73696720;
      private static final int ICC_TAG_TYPE_MEAS = 0x6D656173;
      private static final int ICC_TAG_TYPE_XYZ_ARRAY = 0x58595A20;
      private static final int ICC_TAG_TYPE_MLUC = 0x6d6c7563;
      private static final int ICC_TAG_TYPE_CURV = 0x63757276;
  
      @Nullable
      private String getTagDataString(int tagType)
      {
          try {
              byte[] bytes = _directory.getByteArray(tagType);
              if (bytes == null)
                  return _directory.getString(tagType);
              RandomAccessReader reader = new ByteArrayReader(bytes);
              int iccTagType = reader.getInt32(0);
              switch (iccTagType) {
                  case ICC_TAG_TYPE_TEXT:
                      try {
                          return new String(bytes, 8, bytes.length - 8 - 1, "ASCII");
                      } catch (UnsupportedEncodingException ex) {
                          return new String(bytes, 8, bytes.length - 8 - 1);
                      }
                  case ICC_TAG_TYPE_DESC:
                      int stringLength = reader.getInt32(8);
  
                      if (stringLength < 0 || stringLength > (bytes.length - 12)) {
                          throw new BufferBoundsException(12, stringLength, bytes.length);
                      }
  
                      return new String(bytes, 12, stringLength - 1);
                  case ICC_TAG_TYPE_SIG:
                      return IccReader.getStringFromInt32(reader.getInt32(8));
                  case ICC_TAG_TYPE_MEAS: {
                      int observerType = reader.getInt32(8);
                      float x = reader.getS15Fixed16(12);
                      float y = reader.getS15Fixed16(16);
                      float z = reader.getS15Fixed16(20);
                      int geometryType = reader.getInt32(24);
                      float flare = reader.getS15Fixed16(28);
                      int illuminantType = reader.getInt32(32);
                      String observerString;
                      switch (observerType) {
                          case 0:
                              observerString = "Unknown";
                              break;
                          case 1:
                              observerString = "1931 2\u00B0";
                              break;
                          case 2:
                              observerString = "1964 10\u00B0";
                              break;
                          default:
                              observerString = String.format("Unknown %d", observerType);
                      }
                      String geometryString;
                      switch (geometryType) {
                          case 0:
                              geometryString = "Unknown";
                              break;
                          case 1:
                              geometryString = "0/45 or 45/0";
                              break;
                          case 2:
                              geometryString = "0/d or d/0";
                              break;
                          default:
                              geometryString = String.format("Unknown %d", observerType);
                      }
                      String illuminantString;
                      switch (illuminantType) {
                          case 0:
                              illuminantString = "unknown";
                              break;
                          case 1:
                              illuminantString = "D50";
                              break;
                          case 2:
                              illuminantString = "D65";
                              break;
                          case 3:
                              illuminantString = "D93";
                              break;
                          case 4:
                              illuminantString = "F2";
                              break;
                          case 5:
                              illuminantString = "D55";
                              break;
                          case 6:
                              illuminantString = "A";
                              break;
                          case 7:
                              illuminantString = "Equi-Power (E)";
                              break;
                          case 8:
                              illuminantString = "F8";
                              break;
                          default:
                              illuminantString = String.format("Unknown %d", illuminantType);
                              break;
                      }
                      DecimalFormat format = new DecimalFormat("0.###");
                      return String.format("%s Observer, Backing (%s, %s, %s), Geometry %s, Flare %d%%, Illuminant %s",
                              observerString, format.format(x), format.format(y), format.format(z), geometryString, Math.round(flare * 100), illuminantString);
                  }
                  case ICC_TAG_TYPE_XYZ_ARRAY: {
                      StringBuilder res = new StringBuilder();
                      DecimalFormat format = new DecimalFormat("0.####");
                      int count = (bytes.length - 8) / 12;
                      for (int i = 0; i < count; i++) {
                          float x = reader.getS15Fixed16(8 + i * 12);
                          float y = reader.getS15Fixed16(8 + i * 12 + 4);
                          float z = reader.getS15Fixed16(8 + i * 12 + 8);
                          if (i > 0)
                              res.append(", ");
                          res.append("(").append(format.format(x)).append(", ").append(format.format(y)).append(", ").append(format.format(z)).append(")");
                      }
                      return res.toString();
                  }
                  case ICC_TAG_TYPE_MLUC: {
                      int int1 = reader.getInt32(8);
                      StringBuilder res = new StringBuilder();
                      res.append(int1);
                      //int int2 = reader.getInt32(12);
                      //System.err.format("int1: %d, int2: %d\n", int1, int2);
                      for (int i = 0; i < int1; i++) {
                          String str = IccReader.getStringFromInt32(reader.getInt32(16 + i * 12));
                          int len = reader.getInt32(16 + i * 12 + 4);
                          int ofs = reader.getInt32(16 + i * 12 + 8);
                          String name;
                          try {
                              name = new String(bytes, ofs, len, "UTF-16BE");
                          } catch (UnsupportedEncodingException ex) {
                              name = new String(bytes, ofs, len);
                          }
                          res.append(" ").append(str).append("(").append(name).append(")");
                          //System.err.format("% 3d: %s, len: %d, ofs: %d, \"%s\"\n", i, str, len,ofs,name);
                      }
                      return res.toString();
                  }
                  case ICC_TAG_TYPE_CURV: {
                      int num = reader.getInt32(8);
                      StringBuilder res = new StringBuilder();
                      for (int i = 0; i < num; i++) {
                          if (i != 0)
                              res.append(", ");
                          res.append(formatDoubleAsString(((float)reader.getUInt16(12 + i * 2)) / 65535.0, 7, false));
                          //res+=String.format("%1.7g",Math.round(((float)iccReader.getInt16(b,12+i*2))/0.065535)/1E7);
                      }
                      return res.toString();
                  }
                  default:
                      return String.format("%s (0x%08X): %d bytes", IccReader.getStringFromInt32(iccTagType), iccTagType, bytes.length);
              }
          } catch (IOException e) {
              // TODO decode these values during IccReader.extract so we can report any errors at that time
              // It is convention to return null if a description cannot be formulated.
              // If an error is to be reported, it should be done during the extraction process.
              return null;
          }
      }
  
      @NotNull
      public static String formatDoubleAsString(double value, int precision, boolean zeroes)
      {
          if (precision < 1)
              return "" + Math.round(value);
          long intPart = Math.abs((long)value);
          long rest = (int)Math.round((Math.abs(value) - intPart) * Math.pow(10, precision));
          long restKept = rest;
          String res = "";
          byte cour;
          for (int i = precision; i > 0; i--) {
              cour = (byte)(Math.abs(rest % 10));
              rest /= 10;
              if (res.length() > 0 || zeroes || cour != 0 || i == 1)
                  res = cour + res;
          }
          intPart += rest;
          boolean isNegative = ((value < 0) && (intPart != 0 || restKept != 0));
          return (isNegative ? "-" : "") + intPart + "." + res;
      }
  
      @Nullable
      private String getRenderingIntentDescription()
      {
          return getIndexedDescription(TAG_RENDERING_INTENT,
              "Perceptual",
              "Media-Relative Colorimetric",
              "Saturation",
              "ICC-Absolute Colorimetric");
      }
  
      @Nullable
      private String getPlatformDescription()
      {
          String str = _directory.getString(TAG_PLATFORM);
          if (str==null)
              return null;
          // Because Java doesn't allow switching on string values, create an integer from the first four chars
          // and switch on that instead.
          int i;
          try {
              i = getInt32FromString(str);
          } catch (IOException e) {
              return str;
          }
          switch (i) {
              case 0x4150504C: // "APPL"
                  return "Apple Computer, Inc.";
              case 0x4D534654: // "MSFT"
                  return "Microsoft Corporation";
              case 0x53474920:
                  return "Silicon Graphics, Inc.";
              case 0x53554E57:
                  return "Sun Microsystems, Inc.";
              case 0x54474E54:
                  return "Taligent, Inc.";
              default:
                  return String.format("Unknown (%s)", str);
          }
      }
  
      @Nullable
      private String getProfileClassDescription()
      {
          String str = _directory.getString(TAG_PROFILE_CLASS);
          if (str==null)
              return null;
          // Because Java doesn't allow switching on string values, create an integer from the first four chars
          // and switch on that instead.
          int i;
          try {
              i = getInt32FromString(str);
          } catch (IOException e) {
              return str;
          }
          switch (i) {
              case 0x73636E72:
                  return "Input Device";
              case 0x6D6E7472: // mntr
                  return "Display Device";
              case 0x70727472:
                  return "Output Device";
              case 0x6C696E6B:
                  return "DeviceLink";
              case 0x73706163:
                  return "ColorSpace Conversion";
              case 0x61627374:
                  return "Abstract";
              case 0x6E6D636C:
                  return "Named Color";
              default:
                  return String.format("Unknown (%s)", str);
          }
      }
  
      @Nullable
      private String getProfileVersionDescription()
      {
          Integer value = _directory.getInteger(TAG_PROFILE_VERSION);
  
          if (value == null)
              return null;
  
          int m = (value & 0xFF000000) >> 24;
          int r = (value & 0x00F00000) >> 20;
          int R = (value & 0x000F0000) >> 16;
  
          return String.format("%d.%d.%d", m, r, R);
      }
  
      private static int getInt32FromString(@NotNull String string) throws IOException
      {
          byte[] bytes = string.getBytes();
          return new ByteArrayReader(bytes).getInt32(0);
      }
  }
  