function count = cprintf(style,format,varargin)
% CPRINTF displays styled formatted text in the Command Window
%
% Syntax:
%    count = cprintf(style,format,...)
%
% Description:
%    CPRINTF processes the specified text using the exact same FORMAT
%    arguments accepted by the built-in SPRINTF and FPRINTF functions.
%
%    CPRINTF then displays the text in the Command Window using the
%    specified STYLE argument. The accepted styles are those used for
%    Matlab's syntax highlighting (see: File / Preferences / Colors / 
%    M-file Syntax Highlighting Colors), and also user-defined colors.
%
%    The possible pre-defined STYLE names are:
%
%       'Text'                 - default: black
%       'Keywords'             - default: blue
%       'Comments'             - default: green
%       'Strings'              - default: purple
%       'UnterminatedStrings'  - default: dark red
%       'SystemCommands'       - default: orange
%       'Errors'               - default: light red
%       'Hyperlinks'           - default: underlined blue
%
%       'Black','Cyan','Magenta','Blue','Green','Red','Yellow','White'
%
%    Note: styles beginning with '-' will be underlined. For example:
%          '-Blue' is underlined blue, like 'Hyperlinks';
%          '-Comments' is underlined green etc.
%
%    STYLE also accepts a regular Matlab RGB vector, that can be negated
%    for underlining. For example: -[0,1,1] means underlined cyan.
%
%    STYLE is case-insensitive and accepts unique partial strings just
%    like handle property names.
%
% Examples:
%    cprintf('text',   'regular black text');
%    cprintf('hyper',  'followed %s','by');
%    cprintf('k',      '%d colored', 4);
%    cprintf('-comment','& underlined');
%    cprintf('err',    'elements\n');
%    cprintf('cyan',   'cyan');
%    cprintf('-green', 'underlined green');
%    cprintf(-[1,0,1], 'underlined magenta');
%    cprintf([1,0.5,0],'and multi-\nline orange\n');
%
% Bugs and suggestions:
%    Please send to Yair Altman (altmany at gmail dot com)
%
% Warning:
%    This code heavily relies on undocumented and unsupported Matlab
%    functionality. It works on Matlab 7+, but use at your own risk!
%
%    A technical description of the implementation can be found at:
%    <a href="http://undocumentedmatlab.com/blog/cprintf/">http://UndocumentedMatlab.com/blog/cprintf/</a>
%
% Limitations:
%    1. For unfortunate implementation reasons, a single space character
%       is inserted at the beginning of each CPRINTF text segment.
%       I hope to solve this in a future version of CPRINTF.
%
%    2. consecutive differently-colored multi-line CPRINTFs sometimes
%       display incorrectly on the bottom line.
%       As far as I could tell this is due to a Matlab bug. Examples:
%         >> cprintf('-str','under\nline'); cprintf('err','red\n'); % hidden 'red', unhidden '_'
%         >> cprintf('str','regu\nlar'); cprintf('err','red\n'); % underline red (not purple) 'lar'
%
%    3. Sometimes, non newline ('\n')-terminated segments display unstyled
%       (black) when the command prompt cevron ('>>') regains focus on the
%       continuation of that line (I can't pinpoint when this happens). 
%       To fix this, simply newline-terminate all command-prompt messages.
%
%    4. Multi-line styles only affect the first line on old Matlab versions
%       (e.g., Matlab 7.1 R14). Single-line styles work as expected.
%       R14 also appends a single space after underlined segments.
%
% Change log:
%    2009-05-13: First version posted on <a href="http://www.mathworks.com/matlabcentral/fileexchange/authors/27420">MathWorks File Exchange</a>
%
% See also:
%    sprintf, fprintf

% License to use and modify this code is granted freely to all interested, as long as the original author is
% referenced and attributed as such. The original author maintains the right to be solely associated with this work.

% Programmed and Copyright by Yair M. Altman: altmany(at)gmail.com
% $Revision$  $Date$

  % The following is for debug use only:
  %global docElement txt el
  if ~exist('el','var') || isempty(el),  el=handle([]);  end  %#ok mlint short-circuit error ("used before defined")
  if isempty(style),  return;  end
  if ishandle(style),  dumpElement(style); return;  end

  % Process the text string
  error(nargchk(2, inf, nargin, 'struct'));
  str = sprintf(format,varargin{:});

  % Get the normalized style name and underlining flag
  [underlineFlag, style] = processStyleInfo(style);

  % Set hyperlinking, is so requested
  if underlineFlag
      str = ['<a href=" ">' str '</a>'];
      v = version;
      
      % Matlab 7.1 R14 (possibly a few newer versions as well?)
      % have a bug in rendering consecutive hyperlinks
      % This is fixed by appending a single non-linked space
      if str2double(v(1:3)) <= 7.1
          str(end+1) = ' ';
      end
  end

  % Get the current CW position
  cmdWinDoc = com.mathworks.mde.cmdwin.CmdWinDocument.getInstance;
  lastPos = cmdWinDoc.getLength;

  % If not beginning of line
  bolFlag = 0;  %#ok
  %if docElement.getEndOffset - docElement.getStartOffset > 1
      % Display a hyperlink element in order to force element separation
      % (otherwise adjacent elements on the same line will me merged)
      if ~underlineFlag
          fprintf('<a href=""> </a>');
      else
          fprintf(' ');
      end
      drawnow;
      bolFlag = 1;
  %end

  % Get a handle to the Command Window component
  mde = com.mathworks.mde.desk.MLDesktop.getInstance;
  cw = mde.getClient('Command Window');
  xCmdWndView = cw.getComponent(0).getViewport.getComponent(0);

  % Store the CW background color as a special color pref
  % This way, if the CW bg color changes (via File/Preferences), 
  % it will also affect existing rendered strs
  com.mathworks.services.Prefs.setColorPref('CW_BG_Color',xCmdWndView.getBackground);

  % Display the text in the Command Window
  count = fprintf(2,str);
  %awtinvoke(cmdWinDoc,'remove',lastPos,1);   % TODO: find out how to remove the extra '_'
  drawnow;
  docElement = cmdWinDoc.getParagraphElement(lastPos+1);
  if bolFlag && ~underlineFlag
      % Set the leading hyperlink space character ('_') to the bg color, effectively hiding it
      % Note: old Matlab versions have a bug in hyperlinks that need to be accounted for...
      %disp(' '); dumpElement(docElement)
      setElementStyle(docElement,'CW_BG_Color',1+underlineFlag); %+getUrlsFix(docElement));
      %disp(' '); dumpElement(docElement)
      el(end+1) = handle(docElement);
  end

  % Fix a problem with some hidden hyperlinks becoming unhidden...
  fixHyperlink(docElement);

  % Get the Document Element(s) corresponding to the latest fprintf operation
  while docElement.getStartOffset < cmdWinDoc.getLength
      % Set the element style according to the current style
      setElementStyle(docElement,style,underlineFlag);
      docElement2 = cmdWinDoc.getParagraphElement(docElement.getEndOffset+1);
      if isequal(docElement,docElement2),  break;  end
      docElement = docElement2;
  end

  % Force a Command-Window repaint
  % Note: this is important in case the rendered str was not '\n'-terminated
  xCmdWndView.repaint;

  % The following is for debug use only:
  el(end+1) = handle(docElement);  %#ok used in debug only
  %elementStart = docElement.getStartOffset;
  %elementLength = docElement.getEndOffset - elementStart;
  %txt = cmdWinDoc.getText(elementStart,elementLength);

  return;  % debug breakpoint

% Process the requested style information
function [underlineFlag,style] = processStyleInfo(style)
  underlineFlag = 0;

  % Style = valid matlab RGB vector
  if isnumeric(style) && length(style)==3 && all(style<=1) && all(abs(style)>=0)
      if any(style<0)
          underlineFlag = 1;
          style = abs(style);
      end
      style = getColorStyle(style);

  elseif ~ischar(style)
      error('YMA:cprintf:InvalidStyle','Invalid style - see help section for a list of valid style values')

  % Style name
  else
      % Styles starting with '-' should be underlined (using a no-target hyperlink hack)
      if style(1)=='-'
          underlineFlag = 1;
          style = style(2:end);
      end

      % Try case-insensitive partial/full match with the accepted style names
      validStyles = {'Text','Keywords','Comments','Strings','UnterminatedStrings','SystemCommands','Errors', ...
                     'Black','Cyan','Magenta','Blue','Green','Red','Yellow','White', ...
                     'Hyperlinks'};
      matches = find(strncmpi(style,validStyles,length(style)));

      % No match - error
      if isempty(matches)
          error('YMA:cprintf:InvalidStyle','Invalid style - see help section for a list of valid style values')

      % Too many matches (ambiguous) - error
      elseif length(matches) > 1
          error('YMA:cprintf:AmbigStyle','Ambiguous style name - supply extra characters for uniqueness')

      % Regular text
      elseif matches == 1
          style = '';

      % Highlight preference style name
      elseif matches < 8
          style = ['Colors_M_' validStyles{matches}];

      % Color name
      elseif matches < length(validStyles)
          colors = [0,0,0; 0,1,1; 1,0,1; 0,0,1; 0,1,0; 1,0,0; 1,1,0; 1,1,1];
          requestedColor = colors(matches-7,:);
          style = getColorStyle(requestedColor);

      % Hyperlink
      else
          style = 'Colors_HTML_HTMLLinks';  % CWLink
          underlineFlag = 1;
      end
  end

% Convert a Matlab RGB vector into a known style name (e.g., '[255,37,0]')
function styleName = getColorStyle(rgb)
  intColor = int32(round(rgb*255));
  javaColor = java.awt.Color(intColor(1), intColor(2), intColor(3));
  styleName = sprintf('[%d,%d,%d]',intColor);
  com.mathworks.services.Prefs.setColorPref(styleName,javaColor);

% Fix a bug in some Matlab versions, where the number of URL segments
% is larger than the number of style segments in a doc element
function delta = getUrlsFix(docElement)  %#ok currently unused
  tokens = docElement.getAttribute('SyntaxTokens');
  links  = docElement.getAttribute('LinkStartTokens');
  if length(links) > length(tokens(1))
      delta = length(links) > length(tokens(1));
  else
      delta = 0;
  end

% fprintf(2,str) causes all previous '_'s in the line to become red - fix this
function fixHyperlink(docElement)
  try
      tokens = docElement.getAttribute('SyntaxTokens');
      urls   = docElement.getAttribute('HtmlLink');
      urls   = urls(2);
      links  = docElement.getAttribute('LinkStartTokens');
      offsets = tokens(1);
      styles  = tokens(2);
      doc = docElement.getDocument;

      % Loop over all segments in this docElement
      for idx = 1 : length(offsets)-1
          % If this is a hyperlink with no URL target and starts with ' ' and is collored as an error (red)...
          if strcmp(styles(idx).char,'Colors_M_Errors')
              character = char(doc.getText(offsets(idx)+docElement.getStartOffset,1));
              if strcmp(character,' ')
                  if isempty(urls(idx)) && links(idx)==0
                      % Revert the style color to the CW background color (i.e., hide it!)
                      styles(idx) = java.lang.String('CW_BG_Color');
                  end
              end
          end
      end
  catch
      % never mind...
  end

% Set an element to a particular style (color)
function setElementStyle(docElement,style,hyperlinkFlag)
  %global tokens links urls urlTargets  % for debug only
  if nargin<3,  hyperlinkFlag=0;  end
  % Set the last Element token to the requested style:
  % Colors:
  tokens = docElement.getAttribute('SyntaxTokens');
  try
      styles = tokens(2);
      %hyperlinkFlag = ~isempty(strmatch('CWLink',tokens(2)));
      %hyperlinkFlag = 0 + any(cellfun(@(c)(~isempty(c)&&strcmp(c,'CWLink')),tokens(2).cell));
      styles(end) = java.lang.String('');
      styles(end-hyperlinkFlag) = java.lang.String(style);  %#ok apparently unused but in reality used by Java
  catch
      % never mind for now
  end
  
  % Underlines (hyperlinks):
  links = docElement.getAttribute('LinkStartTokens');
  if isempty(links)
      %docElement.addAttribute('LinkStartTokens',repmat(int32(-1),length(tokens(2)),1));
  else
      %TODO: remove hyperlink by setting the value to -1
  end
  
  % Correct empty URLs to be un-hyperlinkable (only underlined)
  urls  = docElement.getAttribute('HtmlLink');
  if isempty(urls),  return;  end
  urlTargets = urls(2);
  for urlIdx = 1 : length(urlTargets)
      try
          if urlTargets(urlIdx).length < 1
              urlTargets(urlIdx) = [];  % '' => []
          end
      catch
          % never mind...
          a=1;  %#ok used for debug breakpoint...
      end
  end

% Display information about element(s)
function dumpElement(docElements)
  %return;
  numElements = length(docElements);
  for elementIdx = 1 : numElements
      if numElements > 1,  fprintf('Element #%d:\n',elementIdx);  end
      docElement = docElements(elementIdx);
      if ~isjava(docElement),  docElement = docElement.java;  end
      %docElement.dump(java.lang.System.out,1)
      tokens = docElement.getAttribute('SyntaxTokens');
      if isempty(tokens),  continue;  end
      links = docElement.getAttribute('LinkStartTokens');
      urls  = docElement.getAttribute('HtmlLink');
      try
          data = [tokens(2).cell m2c(tokens(1)) m2c(links) m2c(urls(1)) cell(urls(2))];
      catch
          try
              data = [tokens(2).cell m2c(tokens(1)) m2c(links)];
          catch
              disp([tokens(2).cell m2c(tokens(1))]);
              try
                  data = [m2c(links) m2c(urls(1)) cell(urls(2))];
              catch
                  % Mtlab 7.1 only has urls(1)...
                  data = [m2c(links) urls.cell];
              end
          end
      end
      disp(' ');
      disp(docElement)
      disp(data)
  end

% Utility function to convert matrix => cell
function cells = m2c(data)
  datasize = size(data);
  cells = mat2cell(data,ones(1,datasize(1)),ones(1,datasize(2)));


%%%%%%%%%%%%%%%%%%%%%%%%%% TODO %%%%%%%%%%%%%%%%%%%%%%%%%
% - Fix: Remove leading space char (hidden underline '_')
% - Fix: Find workaround for multi-line quirks/limitations
% - Fix: Non-\n-terminated segments are displayed as black
% - Fix: Check whether the hyperlink fix for 7.1 is also needed on 7.2 etc.