using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cosmos.Common.Extensions;

namespace Cosmos.System.Filesystem.FAT {
  public class FatFileSystem : FileSystem {
    readonly public UInt32 BytesPerSector;
    readonly public UInt32 SectorsPerCluster;
    readonly public UInt32 BytesPerCluster;
    
    readonly public UInt32 ReservedSectorCount;
    readonly public UInt32 TotalSectorCount;
    readonly public UInt32 ClusterCount;
    
    readonly public UInt32 NumberOfFATs;
    readonly public UInt32 FatSectorCount;

    readonly public UInt32 RootSector = 0; // FAT12/16
    readonly public UInt32 RootSectorCount = 0; // FAT12/16, FAT32 remains 0
    readonly public UInt32 RootCluster; // FAT32
    readonly public UInt32 RootEntryCount;

    readonly public UInt32 DataSector; // First Data Sector
    readonly public UInt32 DataSectorCount;

    public static class Attribs {
      public const int Test = 0x01;
      public const int Hidden = 0x02;
      public const int System = 0x04;
      public const int VolumeID = 0x08;
      public const int Directory = 0x10;
      public const int Archive = 0x20;
      // LongName was created after and is a combination of other attribs. Its "special".
      public const int LongName = 0x0F;
    }

    public enum FatTypeEnum {Unknown, Fat12, Fat16, Fat32}
    readonly public FatTypeEnum FatType = FatTypeEnum.Unknown;

    Cosmos.Hardware.BlockDevice.BlockDevice mDevice;

    public void ReadFatTableSector(UInt32 xSectorNum, byte[] aData) {
      mDevice.ReadBlock(ReservedSectorCount + xSectorNum, 1, aData);
    }

    public bool FatEntryIsEOF(UInt32 aValue) {
      if (FatType == FatTypeEnum.Fat12) {
        return aValue >= 0x0FF8;
      } else if (FatType == FatTypeEnum.Fat16) {
        return aValue >= 0xFFF8;
      } else {
        return aValue >= 0x0FFFFFF8;
      }
    }

    public UInt32 GetFatEntry(byte[] aSector, UInt32 aClusterNum, UInt32 aOffset) {
      if (FatType == FatTypeEnum.Fat12) {
        if (aOffset == (BytesPerSector - 1)) {
          throw new Exception("TODO: Sector Span");
          /* This cluster access spans a sector boundary in the FAT */
          /* There are a number of strategies to handling this. The */
          /* easiest is to always load FAT sectors into memory */
          /* in pairs if the volume is FAT12 (if you want to load */
          /* FAT sector N, you also load FAT sector N+1 immediately */
          /* following it in memory unless sector N is the last FAT */
          /* sector). It is assumed that this is the strategy used here */
          /* which makes this if test for a sector boundary span */
          /* unnecessary. */
        }
        // We now access the FAT entry as a WORD just as we do for FAT16, but if the cluster number is
        // EVEN, we only want the low 12-bits of the 16-bits we fetch. If the cluster number is ODD
        // we want the high 12-bits of the 16-bits we fetch. 
        UInt32 xResult = aSector.ToUInt16(aOffset);
        if ((aClusterNum & 0x01) == 0) { // Even
          return xResult & 0x0FFF;
        } else { // Odd
          return xResult >> 4;
        }
      } else if (FatType == FatTypeEnum.Fat16) {
        return aSector.ToUInt16(aOffset);
      } else {
        return aSector.ToUInt32(aOffset) & 0x0FFFFFFF;
      }
    }

    public FatFileSystem(Cosmos.Hardware.BlockDevice.BlockDevice aDevice) {
      mDevice = aDevice;

      var xBPB = aDevice.NewBlockArray(1);
      mDevice.ReadBlock(0, 1, xBPB);

      UInt16 xSig = xBPB.ToUInt16(510);
      if (xSig != 0xAA55) {
        throw new Exception("FAT signature not found.");
      }

      BytesPerSector = xBPB.ToUInt16(11);
      SectorsPerCluster = xBPB[13];
      BytesPerCluster = BytesPerSector * SectorsPerCluster;
      ReservedSectorCount = xBPB.ToUInt16(14);
      NumberOfFATs = xBPB[16];
      RootEntryCount = xBPB.ToUInt16(17);

      TotalSectorCount = xBPB.ToUInt16(19);
      if (TotalSectorCount == 0) {
        TotalSectorCount = xBPB.ToUInt32(32);
      }

      // FATSz
      FatSectorCount = xBPB.ToUInt16(22);
      if (FatSectorCount == 0) {
        FatSectorCount = xBPB.ToUInt32(36);
      }
      Global.Dbg.Send("FAT Sector Count: " + FatSectorCount);

      DataSectorCount = TotalSectorCount - (ReservedSectorCount + (NumberOfFATs * FatSectorCount) + ReservedSectorCount);
 
      // Computation rounds down. 
      ClusterCount = DataSectorCount / SectorsPerCluster;
      // Determine the FAT type. Do not use another method - this IS the official and
      // proper way to determine FAT type.
      // Comparisons are purposefully < and not <=
      // FAT16 starts at 4085, FAT32 starts at 65525 
      if (ClusterCount < 4085) {
        FatType = FatTypeEnum.Fat12;
      } else if(ClusterCount < 65525) {
        FatType = FatTypeEnum.Fat16;
      } else {
        FatType = FatTypeEnum.Fat32;
      }

      if (FatType == FatTypeEnum.Fat32) {
        RootCluster = xBPB.ToUInt32(44);
      } else {
        RootSector = ReservedSectorCount + (NumberOfFATs * FatSectorCount);
        RootSectorCount = (RootEntryCount * 32 + (BytesPerSector - 1)) / BytesPerSector;
      }
      DataSector = ReservedSectorCount + (NumberOfFATs * FatSectorCount) + RootSectorCount;
    }

    public byte[] NewClusterArray() {
      return new byte[BytesPerCluster];
    }

    public void ReadCluster(UInt32 aCluster, byte[] aData) {
      UInt64 xSector = DataSector + ((aCluster - 2) * SectorsPerCluster);
      mDevice.ReadBlock(xSector, SectorsPerCluster, aData);
    }

    public void GetFatTableSector(UInt32 aClusterNum, out UInt32 oSector, out UInt32 oOffset) {
      UInt32 xOffset = 0;
      if (FatType == FatTypeEnum.Fat12) {
        // Multiply by 1.5 without using floating point, the divide by 2 rounds DOWN
        xOffset = aClusterNum + (aClusterNum / 2);
      } else if (FatType == FatTypeEnum.Fat16) {
        xOffset = aClusterNum * 2;
      } else if (FatType == FatTypeEnum.Fat32) {
        xOffset = aClusterNum * 4;
      }
      oSector = xOffset / BytesPerSector;
      oOffset = xOffset % BytesPerSector;
    }

    public List<Cosmos.System.Filesystem.Listing.Base> GetRoot() {
      var xResult = new List<Cosmos.System.Filesystem.Listing.Base>();

      byte[] xData;
      if (FatType == FatTypeEnum.Fat32) {
        xData = NewClusterArray();
        ReadCluster(RootCluster, xData);
      } else {
        xData = mDevice.NewBlockArray(RootSectorCount);
        mDevice.ReadBlock(RootSector, RootSectorCount, xData);
      }
      //TODO: Change xLongName to StringBuilder
      string xLongName = "";
      for (UInt32 i = 0; i < xData.Length; i = i + 32) {
        byte xAttrib = xData[i + 11];
        if (xAttrib == Attribs.LongName) {
          byte xType = xData[i + 12];
          if (xType == 0) {
            byte xOrd = xData[i];
            if ((xOrd & 0x40) > 0) {
              xLongName = "";
            }
            //TODO: Check LDIR_Ord for ordering and throw exception
            // if entries are found out of order.
            // Also save buffer and only copy name if a end Ord marker is found.
            string xLongPart = xData.GetUtf16String(i + 1, 5);
              // We have to check the length because 0xFFFF is a valid Unicode codepoint.
              // So we only want to stop if the 0xFFFF is AFTER a 0x0000. We can determin
              // this by also looking at the length. Since we short circuit the or, the length
              // is rarely evaluated.
            if (xData.ToUInt16(i + 14) != 0xFFFF || xLongPart.Length == 5) {
              xLongPart = xLongPart + xData.GetUtf16String(i + 14, 6);
              if (xData.ToUInt16(i + 28) != 0xFFFF || xLongPart.Length == 11) {
                xLongPart = xLongPart + xData.GetUtf16String(i + 28, 2);
              }
            }
            xLongName = xLongPart + xLongName;
            //TODO: LDIR_Chksum 
          }
        } else {
          byte xStatus = xData[i];
          if (xStatus == 0x00) {
            // Empty slot, and no more entries after this
            break;
          } else if (xStatus == 0x05) {
            // Japanese characters - We dont handle these
          } else if (xStatus == 0xE5) {
            // Empty slot, skip it
          } else if (xStatus >= 0x20) {
            string xName;
            if (xLongName.Length > 0) {
              // Leading and trailing spaces are to be ignored according to spec.
              // Many programs (including Windows) pad trailing spaces although it 
              // it is not required for long names.
              // TODO: As per spec, ignore trailing periods
              xName = xLongName.Trim();
            } else {
              string xEntry = xData.GetAsciiString(i, 11);
              xName = xEntry.Substring(0, 8).TrimEnd();
              string xExt = xEntry.Substring(8, 3).TrimEnd();
              if (xExt.Length > 0) {
                xName = xName + "." + xExt;
              }
            }

            UInt32 xFirstCluster = (UInt32)(xData.ToUInt16(i + 20) << 16 | xData.ToUInt16(i + 26));

            var xTest = xAttrib & (Attribs.Directory | Attribs.VolumeID);
            if (xTest == 0) {
              UInt32 xSize = xData.ToUInt32(i + 28);
              xResult.Add(new Listing.FatFile(this, xName, xSize, xFirstCluster));
            } else if (xTest == Attribs.VolumeID) {
              //
            } else if (xTest == Attribs.Directory) {
              xResult.Add(new Listing.FatDirectory(this, xName));
            }
            xLongName = "";
          }
        }
      }

      return xResult;
    }

  }
}
