// SPDX-License-Identifier: LGPL-3.0-linking-exception
var
  PaletteFormats : array of record
    formatIndex: TBGRAPaletteFormat;
    ext: string;
    description: string;
    reader: TPaletteReaderProc;
    writer: TPaletteWriterProc;
    checkFormat: TCheckPaletteFormatProc;
  end;

const
  GimpPaletteHeader : string = 'GIMP Palette';
  KOfficePaletteHeader : string = 'KDE RGB Palette';
  AdobeSwatchExchangeHeader : string = 'ASEF';
  JascPaletteHeader : string = 'JASC-PAL';
  PaintDotNetPaletteHeader : string = '; paint.net Palette File';
  PaintDotNetPaletteHeaderUTF8 : string = #$EF#$BB#$BF + '; paint.net Palette File';

procedure SaveToStreamAsPaintDotNet(APalette: TBGRAPalette; AStream: TStream);

  procedure WriteStr(s: string);
  begin
    AStream.WriteBuffer(s[1],length(S));
  end;
  procedure WriteStrLn(s: string);
  begin
    WriteStr(s+#$0D#$0A);
  end;

var
  i: Integer;

begin
  WriteStrLn(PaintDotNetPaletteHeaderUTF8);
  for i := 0 to APalette.Count-1 do
  with APalette.Color[i] do
    WriteStrLn(IntToHex(alpha,2)+IntToHex(red,2)+IntToHex(green,2)+IntToHex(blue,2));
end;

procedure SaveToStreamAsGimp(APalette: TBGRAPalette; AStream: TStream);
  procedure WriteStr(s: string);
  begin
    AStream.WriteBuffer(s[1],length(S));
  end;
  procedure WriteStrLn(s: string);
  begin
    WriteStr(s+#$0A);
  end;

  procedure WriteChannelValue(AValue: byte);
  var s: string;
  begin
    s := IntToStr(AValue);
    while length(s) < 3 do s := ' '+s;
    WriteStr(s);
  end;

var
  i: Integer;

begin
  WriteStrLn(GimpPaletteHeader);
  WriteStrLn('Name: Palette');
  WriteStrLn('Columns: 3');
  WriteStrLn('#');
  for i := 0 to APalette.Count-1 do
  with APalette.Color[i] do
  begin
    WriteChannelValue(red);
    WriteStr(' ');
    WriteChannelValue(green);
    WriteStr(' ');
    WriteChannelValue(blue);
    WriteStrLn(#$09+BGRAToStr(APalette.Color[i],CSSColors));
  end;
end;

procedure SaveToStreamAsAdobeSwatchExchange(APalette: TBGRAPalette; AStream: TStream);
  procedure WriteStr(s: string);
  begin
    AStream.WriteBuffer(s[1],length(S));
  end;
  procedure WriteInt32(AValue: Int32);
  begin
    AValue := NtoBE(AValue);
    AStream.WriteBuffer(AValue,sizeof(AValue));
  end;
  procedure WriteInt16(AValue: Int16);
  begin
    AValue := NtoBE(AValue);
    AStream.WriteBuffer(AValue,sizeof(AValue));
  end;
  procedure WriteSingle(AValue: Single);
  begin
    LongWord(AValue) := BEtoN(LongWord(AValue));
    AStream.WriteBuffer(AValue,sizeof(AValue));
  end;
  procedure WriteBlock(ABlockType: Int16; AContentLength: Int32); overload;
  begin
    WriteInt16(ABlockType);
    WriteInt32(AContentLength);
  end;

  procedure WriteBlock(ABlockType: Int16; AName: string; AExtraContentLength: Int32); overload;
  var contentLength: Int32;
    wideName: UnicodeString;
    nameBuf: array of byte;
    i: Integer;
  begin
    wideName := UTF8Decode(AName);
    setlength(nameBuf, (length(wideName)+1)*2);
    contentLength:= AExtraContentLength + 2 + length(nameBuf);
    WriteBlock(ABlockType, contentLength);
    WriteInt16(length(nameBuf) shr 1);
    for i := 1 to length(wideName) do
    begin
      nameBuf[((i-1) shl 1)] := ord(wideName[i]) shr 8;
      nameBuf[((i-1) shl 1)+1] := ord(wideName[i]) and 255;
    end;
    AStream.WriteBuffer(nameBuf[0],length(namebuf));
  end;

var
  i: Integer;

begin
  WriteStr(AdobeSwatchExchangeHeader+#$00#$01+#$00#$00);
  WriteInt32(APalette.Count+2); //number of blocks
  WriteBlock($1c0, 'Palette', 0); //group start
  for i := 0 to APalette.Count-1 do
  with APalette.Color[i] do
  begin
    WriteBlock(1, BGRAToStr(APalette.Color[i],CSSColors), 4+4*3+2);
    WriteStr('RGB ');
    WriteSingle(red/255);
    WriteSingle(green/255);
    WriteSingle(blue/255);
    WriteInt16(2); //normal
  end;
  WriteBlock($2c0, 0); //group end
end;

procedure SaveToStreamAsKOffice(APalette: TBGRAPalette; AStream: TStream);
  procedure WriteStr(s: string);
  begin
    AStream.WriteBuffer(s[1],length(S));
  end;
  procedure WriteStrLn(s: string);
  begin
    WriteStr(s+#$0A);
  end;

  procedure WriteChannelValue(AValue: byte);
  begin
    WriteStr(IntToStr(AValue));
  end;

var
  i: Integer;

begin
  WriteStrLn(KOfficePaletteHeader);
  for i := 0 to APalette.Count-1 do
  with APalette.Color[i] do
  begin
    WriteChannelValue(red);
    WriteStr(' ');
    WriteChannelValue(green);
    WriteStr(' ');
    WriteChannelValue(blue);
    WriteStrLn(#$09+BGRAToStr(APalette.Color[i],CSSColors));
  end;
end;

procedure SaveToStreamAsJasc(APalette: TBGRAPalette; AStream: TStream);
  procedure WriteStr(s: string);
  begin
    AStream.WriteBuffer(s[1],length(S));
  end;
  procedure WriteStrLn(s: string);
  begin
    WriteStr(s+#$0D#$0A);
  end;

var
  i: Integer;

begin
  WriteStrLn(JascPaletteHeader);
  WriteStrLn('0100');
  WriteStrLn(IntToStr(APalette.Count));
  for i := 0 to APalette.Count-1 do
  with APalette.Color[i] do
    WriteStrLn(IntToStr(red)+' '+IntToStr(green)+' '+IntToStr(blue));
end;

function LoadFromStreamAsPaintDotNet(APalette: TBGRAPalette; AStream: TStream): boolean;
var lines: TStringList;
  header,s: string;
  idxComment: integer;
  code: integer;
  hexArgb: int32;
  i: Integer;
begin
  result := false;
  lines := TStringList.Create;
  try
    lines.LoadFromStream(AStream);
    if lines.Count = 0 then
    begin
      lines.Free;
      exit;
    end;
    header := lines[0];
    if (header <> PaintDotNetPaletteHeader) and (header <> PaintDotNetPaletteHeaderUTF8) then
    begin
      lines.Free;
      exit;
    end;

    for i := 0 to lines.Count-1 do
    begin
      s := lines[i];
      idxComment := pos(';',s);
      if idxComment<>0 then s := copy(s,1,idxComment-1);
      s := trim(s);
      if length(s)> 0 then
      begin
        val('$'+s, hexArgb, code);
        if code = 0 then
          APalette.AddColor(BGRA((hexArgb shr 16) and 255,
                        (hexArgb shr 8) and 255,
                        hexArgb and 255,
                        (hexArgb shr 24) and 255));
      end;
    end;
    result := true;
  finally
    lines.Free;
  end;
end;

function LoadFromStreamAsGimp(APalette: TBGRAPalette; AStream: TStream): boolean;
var lines,line: TStringList;
  s: string;
  idxComment: integer;
  code: integer;
  c: TBGRAPixel;
  i: Integer;
begin
  result := false;
  lines := TStringList.Create;
  line := TStringList.Create;
  try
    lines.LoadFromStream(AStream);
    if (lines.Count < 3) or (lines[0] <> GimpPaletteHeader) or
      (copy(lines[1],1,6) <> 'Name: ') or
      (copy(lines[2],1,9) <> 'Columns: ') then
    begin
      lines.Free;
      line.Free;
      exit;
    end;
    for i := 3 to lines.Count-1 do
    begin
      s := lines[i];
      idxComment := pos('#',s);
      if idxComment<>0 then s := copy(s,1,idxComment-1);
      s := trim(s);
      if length(s)> 0 then
      begin
        line.CommaText := s;
        if line.Count >= 3 then
        begin
          c.alpha:= 255;
          val(line[0],c.red,code);
          if code <> 0 then continue;
          val(line[1],c.green,code);
          if code <> 0 then continue;
          val(line[2],c.blue,code);
          if code <> 0 then continue;
          APalette.AddColor(c);
        end;
      end;
    end;
    result := true;
  finally
    lines.Free;
    line.Free;
  end;
end;

function clamp(AValue, AMax: integer): integer;
begin
  if AValue < 0 then result := 0 else
  if AValue > AMax then result := AMax else
   result := AValue;;
end;

function LabToRGB(L,a,b: single): TBGRAPixel; overload;
var r,g,blue: single;
begin
  if a < 0 then
    r := L + a + 0.5*b
  else
    r := L + 0.75*a + 0.5*b;
  g := L - 0.5*a;
  blue := L - b;
  Result.red:= clamp(round((r)*255),255);
  Result.green:= clamp(round((g)*255),255);
  Result.blue:= clamp(round((blue)*255),255);
  result.alpha := 255;
end;

function LoadFromStreamAsAdobeSwatchExchange(APalette: TBGRAPalette; AStream: TStream): boolean;
  function ReadInt16: int16;
  begin
    {$PUSH}{$HINTS OFF}
    AStream.Read(result, sizeof(result));
    {$POP}
    result := BEtoN(result);
  end;
  function ReadInt32: int32;
  begin
    {$PUSH}{$HINTS OFF}
    AStream.Read(result, sizeof(result));
    {$POP}
    result := BEtoN(result);
  end;
  function ReadStr(ALength: integer): string;
  begin
    setlength(result, ALength);
    ALength := AStream.Read(result[1], ALength);
    setlength(result, ALength);
  end;
  function ReadSingle: single;
  begin
    {$PUSH}{$HINTS OFF}
    AStream.Read(Result, sizeof(result));
    {$POP}
    LongWord(Result) := BEtoN(LongWord(Result));
  end;
  function DblToByte(AValue: double): byte;
  begin
    if AValue < 0 then result := 0
    else if AValue > 1 then result := 255 else
      result := round(AValue*255);
  end;

var header: string;
  nbBlocks,blockSize: int32;
  blockType,nameLength: int16;
  nextPos: int64;
  colorFormat: string;
  colorF: TColorF;
  i: Integer;
begin
  result := false;
  header := ReadStr(length(AdobeSwatchExchangeHeader)+4);
  if header <> AdobeSwatchExchangeHeader+#$00#$01+#$00#$00 then exit;
  nbBlocks := ReadInt32;
  for i := 0 to nbBlocks-1 do
  begin
    blockType := ReadInt16;
    blockSize := ReadInt32;
    nextPos := AStream.Position + blockSize;
    if blockType = 1 then
    begin
      nameLength := ReadInt16;
      ReadStr(nameLength*2);
      colorFormat := ReadStr(4);
      if colorFormat = 'RGB ' then
      begin
        colorF[1] := ReadSingle;
        colorF[2] := ReadSingle;
        colorF[3] := ReadSingle;
        colorF[4] := 1;
        APalette.AddColor(BGRA(DblToByte(colorF[1]),DblToByte(colorF[2]),DblToByte(colorF[3])));
        ReadInt16; //ignore color type
      end else
      if colorFormat = 'CMYK' then
      begin
        colorF[1] := ReadSingle;
        colorF[2] := ReadSingle;
        colorF[3] := ReadSingle;
        colorF[4] := ReadSingle;

        APalette.AddColor(BGRA(DblToByte(1 - colorF[1] + ColorF[2]/10 + ColorF[3]/10 - ColorF[4]),
          DblToByte(1 - colorF[2] + ColorF[1]/10 + ColorF[3]/10 - ColorF[4]),
          DblToByte(1 - colorF[3] + ColorF[1]/10 + ColorF[2]/10 - ColorF[4])));
        ReadInt16; //ignore color type
      end else
      if colorFormat = 'LAB ' then
      begin
        colorF[1] := ReadSingle;
        colorF[2] := ReadSingle;
        colorF[3] := ReadSingle;
        colorF[4] := 1;

        APalette.AddColor(LabToRGB(colorF[1],colorF[2]/128,colorF[3]/128));
        ReadInt16; //ignore color type
      end else
      if colorFormat = 'GRAY' then
      begin
        colorF[1] := ReadSingle;
        colorF[2] := colorF[1];
        colorF[3] := colorF[1];
        colorF[4] := 1;
        APalette.AddColor(BGRA(DblToByte(colorF[1]),DblToByte(colorF[2]),DblToByte(colorF[3])));
        ReadInt16; //ignore color type
      end;
    end;
    if AStream.Position<>nextPos then
      AStream.Position:= nextPos;
  end;
  result := true;
end;

function LoadFromStreamAsKOffice(APalette: TBGRAPalette; AStream: TStream): boolean;
var lines,line: TStringList;
  s: string;
  idxComment: integer;
  code: integer;
  c: TBGRAPixel;
  i: Integer;
begin
  result := false;
  lines := TStringList.Create;
  line := TStringList.Create;
  try
    lines.LoadFromStream(AStream);
    if (lines.Count < 1) or (lines[0] <> KOfficePaletteHeader) then
    begin
      lines.Free;
      line.Free;
      exit;
    end;
    for i := 3 to lines.Count-1 do
    begin
      s := lines[i];
      idxComment := pos('#',s);
      if idxComment<>0 then s := copy(s,1,idxComment-1);
      s := trim(s);
      if length(s)> 0 then
      begin
        line.CommaText := s;
        if line.Count >= 3 then
        begin
          c.alpha:= 255;
          val(line[0],c.red,code);
          if code <> 0 then continue;
          val(line[1],c.green,code);
          if code <> 0 then continue;
          val(line[2],c.blue,code);
          if code <> 0 then continue;
          APalette.AddColor(c);
        end;
      end;
    end;
    result := true;
  finally
    lines.Free;
    line.Free;
  end;
end;

function LoadFromStreamAsJasc(APalette: TBGRAPalette; AStream: TStream): boolean;
var lines,line: TStringList;
  s: string;
  idxComment: integer;
  code: integer;
  c: TBGRAPixel;
  i: Integer;
begin
  result := false;
  lines := TStringList.Create;
  line := TStringList.Create;
  try
    lines.LoadFromStream(AStream);
    if (lines.Count < 2) or (lines[0] <> JascPaletteHeader) or
     (lines[1] <> '0100') then
    begin
      lines.Free;
      line.Free;
      exit;
    end;
    for i := 2 to lines.Count-1 do
    begin
      s := lines[i];
      idxComment := pos('#',s);
      if idxComment<>0 then s := copy(s,1,idxComment-1);
      s := trim(s);
      if length(s)> 0 then
      begin
        line.CommaText := s;
        if line.Count >= 3 then
        begin
          c.alpha:= 255;
          val(line[0],c.red,code);
          if code <> 0 then continue;
          val(line[1],c.green,code);
          if code <> 0 then continue;
          val(line[2],c.blue,code);
          if code <> 0 then continue;
          APalette.AddColor(c);
        end;
      end;
    end;
    result := true;
  finally
    lines.Free;
    line.Free;
  end;
end;

function CheckPaletteFormatAsJasc(ABuf256: string): boolean;
begin
  result := (copy(ABuf256,1,length(JascPaletteHeader)+1) = JascPaletteHeader+#$0A) or
    (copy(ABuf256,1,length(JascPaletteHeader)+2) = JascPaletteHeader+#$0D#$0A);
end;

function CheckPaletteFormatAsGimp(ABuf256: string): boolean;
begin
  result := (copy(ABuf256,1,length(GimpPaletteHeader)+1) = GimpPaletteHeader+#$0A) or
    (copy(ABuf256,1,length(GimpPaletteHeader)+2) = GimpPaletteHeader+#$0D#$0A);
end;

function CheckPaletteFormatAsKOffice(ABuf256: string): boolean;
begin
  result := (copy(ABuf256,1,length(KOfficePaletteHeader)+1) = KOfficePaletteHeader+#$0A) or
    (copy(ABuf256,1,length(KOfficePaletteHeader)+2) = KOfficePaletteHeader+#$0D#$0A);
end;

function CheckPaletteFormatAsPaintDotNet(ABuf256: string): boolean;
begin
  result := (copy(ABuf256,1,length(PaintDotNetPaletteHeader)+1) = PaintDotNetPaletteHeader+#$0A) or
    (copy(ABuf256,1,length(PaintDotNetPaletteHeader)+2) = PaintDotNetPaletteHeader+#$0D#$0A) or
    (copy(ABuf256,1,length(PaintDotNetPaletteHeaderUTF8)+1) = PaintDotNetPaletteHeaderUTF8+#$0A) or
    (copy(ABuf256,1,length(PaintDotNetPaletteHeaderUTF8)+2) = PaintDotNetPaletteHeaderUTF8+#$0D#$0A);
end;

function CheckPaletteFormatAsAdobeSwatchExchange(ABuf256: string): boolean;
begin
  result := copy(ABuf256,1,length(AdobeSwatchExchangeHeader)) = AdobeSwatchExchangeHeader;
end;


procedure RegisterDefaultPaletteFormats; forward;

procedure BGRARegisterPaletteFormat(AFormatIndex: TBGRAPaletteFormat; AExtension: string;
  ADescription: string; AReadProc: TPaletteReaderProc; AWriteProc: TPaletteWriterProc;
  ACheckFormatProc: TCheckPaletteFormatProc);
var
  i: Integer;
begin
  RegisterDefaultPaletteFormats;
  if AFormatIndex = palUnknown then
    raise Exception.Create('Invalid format index');
  for i := 0 to high(PaletteFormats) do
    if PaletteFormats[i].formatIndex = AFormatIndex then
    with PaletteFormats[i] do
    begin
      ext := AExtension;
      description := ADescription;
      reader := AReadProc;
      writer := AWriteProc;
      checkFormat := ACheckFormatProc;
      exit;
    end;
  setlength(PaletteFormats,length(PaletteFormats)+1);
  with PaletteFormats[high(PaletteFormats)] do
  begin
    formatIndex:= AFormatIndex;
    ext := AExtension;
    description := ADescription;
    reader := AReadProc;
    writer := AWriteProc;
    checkFormat := ACheckFormatProc;
  end;
end;

function BGRARegisteredPaletteFormatFilter(AAllSupportedDescription: string): string;
var allExt: TStringList;
  allDesc: string;
  i: Integer;
begin
  result := '';
  RegisterDefaultPaletteFormats;
  allExt := TStringList.Create;
  allExt.CaseSensitive := false;
  for i := 0 to high(PaletteFormats) do
  with PaletteFormats[i] do
  begin
    if allExt.IndexOf(ext) = -1 then allExt.Add(ext);
    if length(result)>0 then AppendStr(result, '|');
    AppendStr(result, description + ' (*'+ext+')|*'+ext);
  end;
  if allExt.Count > 0 then
  begin
    allDesc := AAllSupportedDescription + ' (';
    for i := 0 to allExt.count-1 do
    begin
      if i > 0 then
        AppendStr(allDesc, '; ');
      AppendStr(allDesc, '*' + allExt[i]);
    end;
    AppendStr(allDesc, ')');
    AppendStr(allDesc, '|');
    for i := 0 to allExt.count-1 do
    begin
      if i > 0 then
        AppendStr(allDesc, '; ');
      AppendStr(allDesc, '*' + allExt[i]);
    end;
    result := allDesc + '|' + result;
  end;
  allExt.Free;
end;

var DefaultPaletteFormatsRegistered: boolean;

procedure RegisterDefaultPaletteFormats;
begin
  if DefaultPaletteFormatsRegistered then exit;
  DefaultPaletteFormatsRegistered := true;
  BGRARegisterPaletteFormat(palPaintDotNet, '.txt', 'Paint.NET',
                            @LoadFromStreamAsPaintDotNet, @SaveToStreamAsPaintDotNet,
                            @CheckPaletteFormatAsPaintDotNet);
  BGRARegisterPaletteFormat(palGimp, '.gpl', 'GIMP',
                            @LoadFromStreamAsGimp, @SaveToStreamAsGimp,
                            @CheckPaletteFormatAsGimp);
  BGRARegisterPaletteFormat(palAdobeSwatchExchange, '.ase', 'Adobe Swatch Exchange',
                            @LoadFromStreamAsAdobeSwatchExchange, @SaveToStreamAsAdobeSwatchExchange,
                            @CheckPaletteFormatAsAdobeSwatchExchange);
  BGRARegisterPaletteFormat(palKOffice, '.colors', 'KOffice',
                            @LoadFromStreamAsKOffice, @SaveToStreamAsKOffice,
                            @CheckPaletteFormatAsKOffice);
  BGRARegisterPaletteFormat(palJascPSP, '.pal', 'Jasc Paint Shop Pro',
                            @LoadFromStreamAsJasc, @SaveToStreamAsJasc,
                            @CheckPaletteFormatAsJasc);
end;

