content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you 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
   *
   *        http://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.
   */
  package org.apache.causeway.extensions.tabular.pdf.factory.internal;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.Stack;
  
  import org.apache.causeway.extensions.tabular.pdf.factory.internal.Token.TokenType;
  
  import lombok.experimental.UtilityClass;
  
  @UtilityClass
  class Tokenizer {
  
  	private static final Token OPEN_TAG_I = new Token(TokenType.OPEN_TAG, "i");
  	private static final Token OPEN_TAG_B = new Token(TokenType.OPEN_TAG, "b");
  	private static final Token OPEN_TAG_OL = new Token(TokenType.OPEN_TAG, "ol");
  	private static final Token OPEN_TAG_UL = new Token(TokenType.OPEN_TAG, "ul");
  	private static final Token CLOSE_TAG_I = new Token(TokenType.CLOSE_TAG, "i");
  	private static final Token CLOSE_TAG_B = new Token(TokenType.CLOSE_TAG, "b");
  	private static final Token CLOSE_TAG_OL = new Token(TokenType.CLOSE_TAG, "ol");
  	private static final Token CLOSE_TAG_UL = new Token(TokenType.CLOSE_TAG, "ul");
  	private static final Token CLOSE_TAG_P = new Token(TokenType.CLOSE_TAG, "p");
  	private static final Token CLOSE_TAG_LI = new Token(TokenType.CLOSE_TAG, "li");
  	private static final Token POSSIBLE_WRAP_POINT = new Token(TokenType.POSSIBLE_WRAP_POINT, "");
  	private static final Token WRAP_POINT_P = new Token(TokenType.WRAP_POINT, "p");
  	private static final Token WRAP_POINT_LI = new Token(TokenType.WRAP_POINT, "li");
  	private static final Token WRAP_POINT_BR = new Token(TokenType.WRAP_POINT, "br");
  
  	public static List<Token> tokenize(final String text, final WrappingFunction wrappingFunction) {
  	    if(text == null) return Collections.emptyList();
  
  		final List<Token> tokens = new ArrayList<>();
  		final Stack<Integer> possibleWrapPoints = wrappingFunction == null
          		? findWrapPoints(text)
          		: findWrapPointsWithFunction(text, wrappingFunction);
          int textIndex = 0;
          final StringBuilder sb = new StringBuilder();
          // taking first wrap point
          Integer currentWrapPoint = possibleWrapPoints.pop();
          while (textIndex < text.length()) {
          	if (textIndex == currentWrapPoint) {
          		if (sb.length() > 0) {
          			tokens.add(Token.text(sb.toString()));
          			sb.delete(0, sb.length());
          		}
          		tokens.add(POSSIBLE_WRAP_POINT);
          		currentWrapPoint = possibleWrapPoints.pop();
          	}
          	final char c = text.charAt(textIndex);
          	switch (c) {
          	case '<':
          		boolean consumed = false;
          		if (textIndex < text.length() - 2) {
          			final char lookahead1 = text.charAt(textIndex + 1);
          			final char lookahead2 = text.charAt(textIndex + 2);
          			if ('i' == lookahead1 && '>' == lookahead2) {
          				// <i>
          				if (sb.length() > 0) {
          					tokens.add(Token.text(sb.toString()));
          					// clean string builder
          					sb.delete(0, sb.length());
          				}
          				tokens.add(OPEN_TAG_I);
          				textIndex += 2;
          				consumed = true;
          			} else if ('b' == lookahead1 && '>' == lookahead2) {
          				// <b>
          				if (sb.length() > 0) {
          					tokens.add(Token.text(sb.toString()));
          					// clean string builder
          					sb.delete(0, sb.length());
          				}
          				tokens.add(OPEN_TAG_B);
          				textIndex += 2;
          				consumed = true;
          			} else if ('b' == lookahead1 && 'r' == lookahead2) {
          				if (textIndex < text.length() - 3) {
          					// <br>
          					final char lookahead3 = text.charAt(textIndex + 3);
          					if (lookahead3 == '>') {
          						if (sb.length() > 0) {
          							tokens.add(Token.text(sb.toString()));
          							// clean string builder
          							sb.delete(0, sb.length());
          						}
          						tokens.add(WRAP_POINT_BR);
          						// normal notation <br>
          						textIndex += 3;
          						consumed = true;
          					} else if (textIndex < text.length() - 4) {
          						// <br/>
          						final char lookahead4 = text.charAt(textIndex + 4);
          						if (lookahead3 == '/' && lookahead4 == '>') {
          							if (sb.length() > 0) {
          								tokens.add(Token.text(sb.toString()));
          								// clean string builder
          								sb.delete(0, sb.length());
          							}
          							tokens.add(WRAP_POINT_BR);
          							// normal notation <br/>
          							textIndex += 4;
          							consumed = true;
          						} else if (textIndex < text.length() - 5) {
          							final char lookahead5 = text.charAt(textIndex + 5);
          							if (lookahead3 == ' ' && lookahead4 == '/' && lookahead5 == '>') {
          								if (sb.length() > 0) {
          									tokens.add(Token.text(sb.toString()));
          									// clean string builder
          									sb.delete(0, sb.length());
          								}
          								tokens.add(WRAP_POINT_BR);
          								// in case it is notation <br />
          								textIndex += 5;
          								consumed = true;
          							}
          						}
          					}
          				}
          			} else if ('p' == lookahead1 && '>' == lookahead2) {
          				// <p>
          				if (sb.length() > 0) {
          					tokens.add(Token.text(sb.toString()));
          					// clean string builder
          					sb.delete(0, sb.length());
          				}
          				tokens.add(WRAP_POINT_P);
          				textIndex += 2;
          				consumed = true;
          			} else if ('o' == lookahead1 && 'l' == lookahead2) {
          				// <ol>
          				if (textIndex < text.length() - 3) {
          					final char lookahead3 = text.charAt(textIndex + 3);
          					if (lookahead3 == '>') {
          						if (sb.length() > 0) {
          							tokens.add(Token.text(sb.toString()));
          							// clean string builder
          							sb.delete(0, sb.length());
          						}
          						tokens.add(OPEN_TAG_OL);
          						textIndex += 3;
          						consumed = true;
          					}
          				}
          			} else if ('u' == lookahead1 && 'l' == lookahead2) {
          				// <ul>
          				if (textIndex < text.length() - 3) {
          					final char lookahead3 = text.charAt(textIndex + 3);
          					if (lookahead3 == '>') {
          						if (sb.length() > 0) {
          							tokens.add(Token.text(sb.toString()));
          							// clean string builder
          							sb.delete(0, sb.length());
          						}
          						tokens.add(OPEN_TAG_UL);
          						textIndex += 3;
          						consumed = true;
          					}
          				}
          			} else if ('l' == lookahead1 && 'i' == lookahead2) {
          				// <li>
          				if (textIndex < text.length() - 3) {
          					final char lookahead3 = text.charAt(textIndex + 3);
          					if (lookahead3 == '>') {
          						if (sb.length() > 0) {
          							tokens.add(Token.text(sb.toString()));
          							// clean string builder
          							sb.delete(0, sb.length());
          						}
          						tokens.add(WRAP_POINT_LI);
          						textIndex += 3;
          						consumed = true;
          					}
          				}
          			} else if ('/' == lookahead1) {
          				// one character tags
          				if (textIndex < text.length() - 3) {
          					final char lookahead3 = text.charAt(textIndex + 3);
          					if ('>' == lookahead3) {
          						if ('i' == lookahead2) {
          							// </i>
          							if (sb.length() > 0) {
          								tokens.add(Token.text(sb.toString()));
          								sb.delete(0, sb.length());
          							}
          							tokens.add(CLOSE_TAG_I);
          							textIndex += 3;
          							consumed = true;
          						} else if ('b' == lookahead2) {
          							// </b>
          							if (sb.length() > 0) {
          								tokens.add(Token.text(sb.toString()));
          								sb.delete(0, sb.length());
          							}
          							tokens.add(CLOSE_TAG_B);
          							textIndex += 3;
          							consumed = true;
          						} else if ('p' == lookahead2) {
          							//</p>
          							if (sb.length() > 0) {
          								tokens.add(Token.text(sb.toString()));
          								sb.delete(0, sb.length());
          							}
          							tokens.add(CLOSE_TAG_P);
          							textIndex += 3;
          							consumed = true;
          						}
          					}
          				}
          				if (textIndex < text.length() - 4) {
          					// lists
          					final char lookahead3 = text.charAt(textIndex + 3);
          					final char lookahead4 = text.charAt(textIndex + 4);
          					if ('l' == lookahead3) {
          						if ('o' == lookahead2 && '>' == lookahead4) {
          							// </ol>
          							if (sb.length() > 0) {
          								tokens.add(Token.text(sb.toString()));
          								sb.delete(0, sb.length());
          							}
          							tokens.add(CLOSE_TAG_OL);
          							textIndex += 4;
          							consumed = true;
          						} else if ('u' == lookahead2 && '>' == lookahead4) {
          							// </ul>
          							if (sb.length() > 0) {
          								tokens.add(Token.text(sb.toString()));
          								sb.delete(0, sb.length());
          							}
          							tokens.add(CLOSE_TAG_UL);
          							textIndex += 4;
          							consumed = true;
          						}
          					} else if ('l' == lookahead2 && 'i' == lookahead3) {
          						// </li>
          						if ('>' == lookahead4) {
          							if (sb.length() > 0) {
          								tokens.add(Token.text(sb.toString()));
          								sb.delete(0, sb.length());
          							}
          							tokens.add(CLOSE_TAG_LI);
          							textIndex += 4;
          							consumed = true;
          						}
          					}
          				}
          			}
  
          		}
          		if (!consumed) {
          			sb.append('<');
          		}
          		break;
          	default:
          		sb.append(c);
          		break;
          	}
          	textIndex++;
          }
  
          if (sb.length() > 0) {
          	tokens.add(Token.text(sb.toString()));
          	sb.delete(0, sb.length());
          }
          tokens.add(POSSIBLE_WRAP_POINT);
  
          return tokens;
  	}
  
  	// -- HELPER
  
  	private static boolean isWrapPointChar(final char ch) {
  	    return
  	            ch == ' '  ||
  	            ch == ','  ||
  	            ch == '.'  ||
  	            ch == '-'  ||
  	            ch == '@'  ||
  	            ch == ':'  ||
  	            ch == ';'  ||
  	            ch == '\n' ||
  	            ch == '\t' ||
  	            ch == '\r' ||
  	            ch == '\f' ||
  	            ch == '\u000B';
  	}
  
  	private static Stack<Integer> findWrapPoints(final String text) {
  	    Stack<Integer> result = new Stack<>();
  	    result.push(text.length());
  	    for (int i = text.length() - 2; i >= 0; i--) {
  	        if (isWrapPointChar(text.charAt(i))) {
  	            result.push(i + 1);
  	        }
  	    }
  	    return result;
  	}
  
  	private static Stack<Integer> findWrapPointsWithFunction(final String text, final WrappingFunction wrappingFunction) {
  	    final String[] split = wrappingFunction.getLines(text);
  	    int textIndex = text.length();
  	    final Stack<Integer> possibleWrapPoints = new Stack<>();
  	    possibleWrapPoints.push(textIndex);
  	    for (int i = split.length - 1; i > 0; i--) {
  	        final int splitLength = split[i].length();
  	        possibleWrapPoints.push(textIndex - splitLength);
  	        textIndex -= splitLength;
  	    }
  	    return possibleWrapPoints;
  	}
  
  }
  