void GenerateVivadoCategorizedTCL(VivadoGeneratorContext generatorContext)
        {
            var generatedPath = generatorContext.generatedPath;

            var vivadoTCL = new IndentedStringBuilder();

            using (Header(vivadoTCL))
            {
                vivadoTCL.AppendLine($"using Quokka.TCL.Tools;");
                vivadoTCL.AppendLine("namespace Quokka.TCL.Vivado");
                using (vivadoTCL.CodeBlock())
                {
                    vivadoTCL.AppendLine($"public partial class VivadoCategorizedTCL : FluentVivadoTCLFile<VivadoCategorizedTCL>");
                    using (vivadoTCL.CodeBlock())
                    {
                        vivadoTCL.AppendLine($"public VivadoCategorizedTCL(VivadoTCLBuilder builder = null) : base(builder)");
                        using (vivadoTCL.CodeBlock())
                        {
                        }

                        foreach (var category in generatorContext.categories)
                        {
                            vivadoTCL.AppendLine($"public {category}Commands<VivadoCategorizedTCL> {category} => new {category}Commands<VivadoCategorizedTCL>(this, _builder);");
                        }
                    }
                }
            }

            var vivadoTCLFilePath = Path.Combine(generatedPath, $"VivadoCategorizedTCL.cs");

            File.WriteAllText(vivadoTCLFilePath, vivadoTCL.ToString());
        }
        public void Generate()
        {
            var path  = Path.Combine(RolloutTools.SolutionLocation(), "docs", pdfName);
            var fixes = Path.Combine(RolloutTools.SolutionLocation(), "docs", fixesName);

            var generatedPath = Path.Combine(RolloutTools.SolutionLocation(), "Quokka.TCL", "Vivado", "generated");

            if (!Directory.Exists(generatedPath))
            {
                Directory.CreateDirectory(generatedPath);
            }

            using (var docReader = DocLib.Instance.GetDocReader(path, new PageDimensions()))
            {
                var generatorContext = new VivadoGeneratorContext()
                {
                    docReader     = docReader,
                    generatedPath = generatedPath,
                    Fixes         = JsonConvert.DeserializeObject <VivadoTCLFixes>(File.ReadAllText(fixes))
                };

                ReadCategoriesAndCommandsSet(generatorContext);
                ReadCommandLines(generatorContext);
                GenerateVivadoTCLBuilder(generatorContext);
                GenerateVivadoTCLCategoriesBuilder(generatorContext);
                GenerateVivadoFlatTCL(generatorContext);
                GenerateVivadoCategorizedTCL(generatorContext);

                var logPath = Path.Combine(generatedPath, "log.md");
                File.WriteAllText(logPath, generatorContext.Log.ToString());
            }
        }
        void GenerateVivadoTCLCategoriesBuilder(VivadoGeneratorContext generatorContext)
        {
            var generatedPath = generatorContext.generatedPath;
            var commandsData  = generatorContext.commandsData;

            foreach (var pair in generatorContext.mapCategoryToCommands)
            {
                var(category, commands) = pair;

                var builder = new IndentedStringBuilder();
                using (Header(builder))
                {
                    builder.AppendLine($"using Quokka.TCL.Tools;");
                    builder.AppendLine($"using System.Collections.Generic;");
                    builder.AppendLine("namespace Quokka.TCL.Vivado");
                    using (builder.CodeBlock())
                    {
                        builder.AppendLine($"public partial class {category}Commands<TTCL> where TTCL : TCLFile");
                        using (builder.CodeBlock())
                        {
                            builder.AppendLine($"private readonly TTCL _tcl;");
                            builder.AppendLine($"private readonly VivadoTCLBuilder _builder;");
                            builder.AppendLine($"public {category}Commands(TTCL tcl, VivadoTCLBuilder builder)");
                            using (builder.CodeBlock())
                            {
                                builder.AppendLine($"_tcl = tcl;");
                                builder.AppendLine($"_builder = builder;");
                            }

                            foreach (var command in commands.OrderBy(c => c))
                            {
                                var commandData = commandsData[command];
                                var record      = new VivadoCommandRecord(generatorContext.Log, generatorContext.Fixes, command, commandData);

                                GenerateCommandCall(record, builder, "TTCL", "_tcl");
                            }
                        }
                    }
                }

                var filePath = Path.Combine(generatedPath, $"{category}.cs");
                File.WriteAllText(filePath, builder.ToString());
            }
        }
        void GenerateVivadoFlatTCL(VivadoGeneratorContext generatorContext)
        {
            var generatedPath = generatorContext.generatedPath;

            var vivadoTCL = new IndentedStringBuilder();

            using (Header(vivadoTCL))
            {
                vivadoTCL.AppendLine($"using Quokka.TCL.Tools;");
                vivadoTCL.AppendLine($"using System.Collections.Generic;");
                vivadoTCL.AppendLine("namespace Quokka.TCL.Vivado");
                using (vivadoTCL.CodeBlock())
                {
                    vivadoTCL.AppendLine($"public partial class VivadoTCL : FluentVivadoTCLFile<VivadoTCL>");
                    using (vivadoTCL.CodeBlock())
                    {
                        vivadoTCL.AppendLine($"public VivadoTCL(VivadoTCLBuilder builder = null) : base(builder)");
                        using (vivadoTCL.CodeBlock())
                        {
                        }

                        foreach (var command in generatorContext.commandsSet.OrderBy(c => c))
                        {
                            var commandData = generatorContext.commandsData[command];
                            var record      = new VivadoCommandRecord(generatorContext.Log, generatorContext.Fixes, command, commandData);

                            GenerateCommandCall(record, vivadoTCL, "VivadoTCL", "this");
                        }
                    }
                }
            }

            var vivadoTCLFilePath = Path.Combine(generatedPath, $"VivadoTCL.cs");

            File.WriteAllText(vivadoTCLFilePath, vivadoTCL.ToString());
        }
        void ReadCategoriesAndCommandsSet(VivadoGeneratorContext generatorContext)
        {
            // find list of commands
            var commandsPage = generatorContext.docReader.GetPageReader(page(21));
            var categories   = generatorContext.categories =
                between(commandsPage.GetText(), "Tcl Command Categories", "Tcl Commands Listed by Category")
                .SelectMany(l => l.Split(" "))
                .ToList();

            var skipLines = new HashSet <string>()
            {
                "Chapter 2",
                "UG835",
                "Tcl Command Reference Guide"
            };

            var commandLines =
                Enumerable.Range(21, 12)
                .SelectMany(p => generatorContext.docReader.GetPageReader(page(p)).GetText().Split('\n').Select(l => l.Trim()))
                .Where(l => !skipLines.Any(s => l.StartsWith(s)))
                .SkipWhile(l => !l.StartsWith("Tcl Commands Listed by Category")).Skip(1)
                .SelectMany(l => l.Split(" ").Select(l => l.Trim()))
                .ToList();

            for (int i = 0; i < categories.Count; i++)
            {
                var category = categories[i];
                var next     = categories.Skip(i + 1).FirstOrDefault() ?? "";
                var commands = between(commandLines, $"{category}:", $"{next}:");
                generatorContext.mapCategoryToCommands[category] = commands;
            }

            var commandsSet = generatorContext.commandsSet = generatorContext.mapCategoryToCommands.SelectMany(p => p.Value).ToHashSet();

            generatorContext.commandsData = commandsSet.ToDictionary(p => p, p => new VivdoCommandTextLines());
        }
        void GenerateVivadoTCLBuilder(VivadoGeneratorContext generatorContext)
        {
            var generatedPath = generatorContext.generatedPath;
            var commandsData  = generatorContext.commandsData;

            var commandsBuilder = new IndentedStringBuilder();

            using (Header(commandsBuilder))
            {
                commandsBuilder.AppendLine($"using Quokka.TCL.Tools;");
                commandsBuilder.AppendLine($"using System.Collections.Generic;");
                commandsBuilder.AppendLine("namespace Quokka.TCL.Vivado");
                using (commandsBuilder.CodeBlock())
                {
                    foreach (var command in commandsData.Keys.OrderBy(k => k))
                    {
                        var commandData = commandsData[command];
                        var record      = new VivadoCommandRecord(generatorContext.Log, generatorContext.Fixes, command, commandData);
                        foreach (var p in record.Parameters.Where(t => t.EnumValues.Any()))
                        {
                            commandsBuilder.AppendLine($"public enum {record.Name}_{p.Name}");
                            using (commandsBuilder.CodeBlock())
                            {
                                foreach (var value in p.EnumValues)
                                {
                                    if (StaticData.keywords.Contains(value))
                                    {
                                        commandsBuilder.AppendLine($"[TCLWrite(\"{value}\")]");
                                        commandsBuilder.AppendLine($"{value.ToUpper()},");
                                        continue;
                                    }


                                    if (value.Contains(" ") || value.Contains("-"))
                                    {
                                        commandsBuilder.AppendLine($"[TCLWrite(\"{value}\")]");
                                        var langValue = Regex.Replace(value, @"[ \-]", "_");
                                        commandsBuilder.AppendLine($"{langValue},");
                                        continue;
                                    }

                                    if (value == "0")
                                    {
                                        commandsBuilder.AppendLine($"[TCLWrite(\"0\")]");
                                        commandsBuilder.AppendLine($"ZERO,");
                                        continue;
                                    }

                                    if (value == "1")
                                    {
                                        commandsBuilder.AppendLine($"[TCLWrite(\"1\")]");
                                        commandsBuilder.AppendLine($"ONE,");
                                        continue;
                                    }

                                    commandsBuilder.AppendLine($"{value},");
                                }
                            }
                        }
                    }

                    commandsBuilder.AppendLine($"public partial class VivadoTCLBuilder");
                    using (commandsBuilder.CodeBlock())
                    {
                        foreach (var command in commandsData.Keys.OrderBy(k => k))
                        {
                            var commandData = commandsData[command];
                            var record      = new VivadoCommandRecord(generatorContext.Log, generatorContext.Fixes, command, commandData);

                            GenerateCommandDeclaration(record, commandsBuilder);
                        }
                    }
                }
            }

            var commandsBuilderPath = Path.Combine(generatedPath, $"VivadoTCLBuilder.cs");

            File.WriteAllText(commandsBuilderPath, commandsBuilder.ToString());
        }
        void ReadCommandLines(VivadoGeneratorContext generatorContext)
        {
            var commandsData = generatorContext.commandsData;

            // extract text for all commands
            var ignoreTextLines = new HashSet <string>()
            {
                "Chapter 3",
                "Tcl Commands Listed Alphabetically",
                "Chapter 3 Tcl Commands Listed Alphabetically",
                "This chapter contains all SDC and Tcl commands, arranged alphabetically.",
                "UG835 (v2019.2) October 30, 2019 www.xilinx.com",
                "Chapter 3: Tcl Commands Listed Alphabetically",
            };

            var breaks = new HashSet <int>()
            {
            };

            string currentCommand = null;

            for (var pageNumber = 33; pageNumber <= 1907; pageNumber++)
            {
                if (breaks.Contains(pageNumber))
                {
                    Debugger.Break();
                }

                var ignorePageTextLines = ignoreTextLines.ToHashSet();
                ignorePageTextLines.Add($"Tcl Command Reference Guide {pageNumber} Send Feedback");

                var reader = generatorContext.docReader.GetPageReader(page(pageNumber));
                var text   = reader.GetText();
                var lines  = text
                             .Split('\n')
                             .Select(l => l.Trim())
                             .Where(l => !ignorePageTextLines.Contains(l))
                             // bunch of hacks and fixes
                             .Select(l => l.Replace((char)8209, '-'))
                             .Select(l => l.Replace((char)65534, '-'))
                             .SelectMany(l =>
                {
                    if (l.StartsWith("-or-"))
                    {
                        return(new[] { "-or-", l.Substring(4) });
                    }
                    else
                    {
                        return(new[] { l });
                    }
                })
                             .ToList();

                // empty page
                if (!lines.Any())
                {
                    continue;
                }

                if (generatorContext.commandsSet.Contains(lines[0]))
                {
                    currentCommand = lines[0];
                    commandsData[currentCommand].Page = pageNumber;
                }

                if (!commandsData.ContainsKey(currentCommand))
                {
                    throw new KeyNotFoundException($"Not a valid command name: {currentCommand}");
                }

                commandsData[currentCommand].Lines.AddRange(lines);
            }

            var missingCommandsText = commandsData.Where(p => !p.Value.Lines.Any()).Select(p => p.Key).ToList();

            if (missingCommandsText.Any())
            {
                throw new Exception($"No description found for commands: {string.Join(", ", missingCommandsText)}");
            }
        }