classdef TiffReader < imageIO.ImageIO
  %TIFFREADER Wrapper for Tiff interface, based on Matlab TIFF class
  %   This class is just a simple wrapper around Matlab Tiff class, adapted
  %   to conform to the structure of the imageIO library. Unlike
  %   TiffWriter, this class does not interface directly with the Tiff
  %   library, since no speed-ups are expected compared to the Matlab
  %   version.
  %   Author: Stefano.Masneri@brain.mpg.de
  %   Date: 29.07.2016
  %   SEE ALSO: imageIO.TiffWriter, Tiff
  
  properties
    tiffPtr;                    % pointer to a Matlab Tiff class
    filePtr;                    % pointer to a file. Used in some special cases
    
    % Other properties. Assuming that for multistack images they remain
    % constant over the stack
    XResolution;                % resolution on horizontal axis
    YResolution;                % resolution on vertical haxis
    resolutionUnit;             % unit of measurement for resolution (none, inch, centimeter)
    bps;                        % bits per sample used
    colormap;                   % colormap used. Empty array if none
    compression;                % compression scheme used
    
    tagNames;                   % Cell of available tags. Useful if the user wants to access 
                                % additonal metadata
    isImageJFmt  = false;       % true if the Tiff is non-standard and was created via imageJ
    isSutterMOM1 = false;       % true if the Tiff is non-standard and from SutterMOM v1
    isSutterMOM2 = false;       % true if the Tiff is non-standard and from SutterMOM v2
    
    endianness='l';             % specify if data is stored as little-endian or big-endian
                                % used only if 'isImageJFmt' is true. Can be either 'l'
                                % or 'b'
    offsetToImg;                % offset to first image in the stack. Used only if isImageJFmt is true
    stripByteCounts;            % number of bytes to reads per strip. Used only if isImageJFmt is true
    
  end
  
  methods
      
    function obj = TiffReader(filename)
    %TIFFREADER Constructor of the class
    %The constructor calls the constructor of the superclass, and then
    %tries to parse the Tiff tags to extract as much information as
    %possible from the file. No actual data is read in the constructor
    %SEE ALSO imageIO.ImageIO.ImageIO
      
      % Must call explicitly because we pass one argument
      obj = obj@imageIO.ImageIO(filename);
      
      % Use Matlab interface
      obj.tiffPtr = Tiff(obj.fileFullPath, 'r');
      
      % Set as many properties from the superclass as possible
      obj = obj.readMetadata();
      
      if obj.isImageJFmt % handle file differently
        obj.filePtr = fopen(obj.fileFullPath);
        endianness = fread(obj.filePtr, 2, '*char')';
        if strcmpi(endianness, 'MM')
          obj.endianness = 'b';
        else %'II'
          obj.endianness = 'l';
        end
        %fseek(obj.filePtr, obj.offsetToImg, 'bof');
      end
    end
    
    function data = read(obj, varargin)
    %READ read all the image data
    %This function reads all the planes of the image. If the file has
    %only one plane just returns that.
    % INPUT
    %   obj: the TiffReader instance
    % NAME-VALUE ARGUMENTS
    %   'Cols': Specify which columns to extract
    %   'Rows': Specify which rows to extract
    %   'C': Specify which channels to extract
    %   'Z': Specify which planes to extract
    %   'T': Specify which timeseries to extract
    % OUTPUT
    %   data: the whole image content
    
      p = inputParser();
      p.KeepUnmatched = true;
      p.addParameter('Cols', 1:obj.width, @(x) isvector(x) && all(x > 0) && max(x) <= obj.pixPerTileCol);
      p.addParameter('Rows', 1:obj.height, @(x) isvector(x) && all(x > 0) && max(x) <= obj.pixPerTileRow);
      p.addParameter('C', 1:obj.channels, @(x) isvector(x) && all(x > 0) && max(x) <= obj.channels);
      p.addParameter('Z', 1:obj.stacks, @(x) isvector(x) && all(x > 0) && max(x) <= obj.stacks);
      p.addParameter('T', 1:obj.time, @(x) isvector(x) && all(x > 0) && max(x) <= obj.time);

      p.parse(varargin{:});
      rows = p.Results.Rows;
      cols = p.Results.Cols;
      channels = p.Results.C;
      stacks = p.Results.Z;
      timeseries = p.Results.T;
            
      if obj.isImageJFmt
        data = readTifImageJ(obj, cols, rows, channels, stacks);
        
      elseif obj.isSutterMOM1 || obj.isSutterMOM2
        data = readSutter(obj, cols, rows, channels, stacks, timeseries );
        
      else % normal tif
        data = zeros(length(rows), length(cols), length(channels), ...
          length(stacks), obj.datatype);
        idx = 1;
        
        progBar = TextProgressBar('TiffReader --> Extracting data: ', 30);

        for k = stacks
          progBar.update(idx/(length(stacks)) * 100);
          img = obj.readImage(k);
          data(:, :, :, idx) = img(rows, cols, channels);
          idx = idx + 1;
        end
      end
      
      data = squeeze(data);
    end
    
    function img = readImage( obj, n )
    %READIMAGE read one image plane
    %This function reads one single plane of the image. If the file has
    %only one plane just returns that.
    % INPUT
    %   n the directory (aka the plane) to read. If bigger than the number
    %     of stacks, issue a warning and return an empty array. If not
    %     specified, return the image in the current directory
    % OUTPUT
    %   img the image just read
    
      if obj.isImageJFmt
        imageSize = obj.height * obj.width * obj.channels;
        precision = [ obj.datatype '=>'  obj.datatype ];
        if n > obj.stacks
          warning('TiffReader.readImage: Cannot read image. n is bigger than the number of stacks')
          img = [];
        else
          if nargin > 1 % n specified
            fseek(obj.filePtr, obj.offsetToImg + (n-1)*imageSize*obj.bps/8, 'bof');
          end
          img = fread(obj.filePtr, imageSize, precision);
          img = reshape(img, [obj.width, obj.height, obj.channels]);
          img = img';
        end
      else
        if 1 == nargin % n not specified
          img = obj.tiffPtr.read();
        elseif ~obj.isSutterMOM1 && ~obj.isSutterMOM2 && n > obj.stacks
          warning('TiffReader.readImage: Cannot read image. n is bigger than the number of stacks')
          img = [];
        else % valid n
          obj.tiffPtr.setDirectory(n);
          img = obj.tiffPtr.read();
        end  
      end
    end
  
    function delete(obj)
    %DELETE Close object instances.
    %Close performs the cleanup and release of the instantiated object
      obj.tiffPtr.close();
      if obj.filePtr > 0
        fclose(obj.filePtr);
      end
    end
  end
  
  methods (Access = protected)
      
    function obj = readMetadata(obj)
        
      %First get usual info with imfinfo
      try
          
        imgInfo = imfinfo(obj.fileFullPath);
        
        % Dimensions
        obj.width = imgInfo(1).Width;
        obj.height = imgInfo(1).Height;
        obj.channels = imgInfo(1).SamplesPerPixel;
        iconIndex = cat(1, imgInfo.NewSubFileType);
        obj.stacks = sum(~iconIndex);
        obj.time = 1;
        obj.bps = imgInfo(1).BitsPerSample(1);
        obj.resolutionUnit = imgInfo(1).ResolutionUnit;
        obj.compression = imgInfo(1).Compression;
        stripOffset = cat(1,imgInfo(iconIndex == 0).StripOffsets);
        obj.offsetToImg = stripOffset(1);
        
        % Standard TIFF does not have multitiled images
        obj.tile = 1;
        obj.numTilesRow = 1;
        obj.numTilesCol = 1;
        obj.rowTilePos = 0;
        obj.colTilePos = 0;
        obj.pixPerTileRow = obj.height;
        obj.pixPerTileCol = obj.width;
        obj.tileOverlap = 0;
        
        % Other info available in imfinfo
        obj.XResolution = imgInfo(1).XResolution;
        obj.YResolution = imgInfo(1).YResolution;
        obj.colormap = imgInfo(1).Colormap;
        
      catch ME
        error('TiffReader.TiffReader: Cannot read metadata. %s', ME.message)
      end
      
      % now use the Tiff pointer
      obj.tagNames = obj.tiffPtr.getTagNames;
      
      % retrieve datatype
      sampleFormat = obj.tiffPtr.getTag('SampleFormat');
      switch sampleFormat
        case 1 % UInt
          obj.datatype = ['uint' num2str(obj.bps)];
        case 2 % Int
          obj.datatype = ['int' num2str(obj.bps)];
        case 3 % IEEEFP
          if 64 == obj.bps
            obj.datatype = 'double';
          elseif 32 == obj.bps
            obj.datatype = 'float';
          else
            warning('TiffReader.readMetadata: unrecognized BitsPerSample value')
          end
        otherwise  % Void or complex types are unsupported
        warning('TiffReader.readMetadata: unsupported sample format')
      end
      
      % check custom multitiff formats -_-'
      try
          
        imageDescription = obj.tiffPtr.getTag('ImageDescription');
        
        % check between formats
        if strncmpi('ImageJ', imageDescription, 6)
            obj.isImageJFmt = true;
            obj.parseMetadataImageJ(imageDescription);

        elseif any(strfind('MOMconfig', imageDescription))
            obj.isSutterMOM1 = true;
            obj.pareMetadataSutter(imageDescription);

        elseif any(strfind('scanimage.SI.', imageDescription))
            obj.isSutterMOM2 = true;
            obj.parseMetadataSutter(imageDescription);

        end
        
        
      catch
          
        % could not retrieve ImageDescription tag
        warning('TiffReader::readMetadata:: Tiff.Tag.ImageDescription is missing!');
        
      end
      
    end
    
    %% decleare external methods
    obj = parseMetadataImageJ(obj, imageDescription);
    obj = parseMetadataSutter(obj, imageDescription);
    
    
  end
  
end