// Split up the G-code file into different segments holding corresponding G-code lines plus feedrate
        // Each segment is created when
        // - the tool number OR
        // - the print region as indicated by S3D ("; feature..." or "; ...")
        // changes.
        // These segments are combined later on in the post-processing step
        public async Task PreProcess()
        {
            StreamReader reader     = new StreamReader(input);
            string       lineBuffer = await reader.ReadLineAsync();

            if (lineBuffer == null)
            {
                throw new ProcessorException("File is empty");
            }

            if (lineBuffer.Contains("G-Code generated by Simplify3D(R)"))
            {
                maxProgress.Report((int)input.Length);

                double       feedrate = DefaultFeedrate;
                double       firstLayerHeight = 0;
                bool         rotaryPrinting = settings.RotaryPrinting != null;
                int          lineNumber = 1, numExtrusions = 0;
                bool         isInterfacingSet = true;
                GCodeLayer   layer = new GCodeLayer(0, 0.0), lastLayer = null;
                GCodeSegment segment = new GCodeSegment("Initialization", -1, null);
                layer.Segments.Add(segment);

                HashSet <int> usedTools = new HashSet <int>();

                do
                {
                    bool      writeLine = true;
                    GCodeLine line      = new GCodeLine(lineBuffer);

                    if (lineBuffer.StartsWith(";", StringComparison.InvariantCulture))
                    {
                        if (lineBuffer.StartsWith("; layer ", StringComparison.InvariantCulture))
                        {
                            segment.LastPosition = lastPoint.Clone();
                            // Add past layer
                            layers.Add(layer);
                            lastLayer = layer;

                            // Get the Z height. S3D provides it via the comment except before the end
                            string lastParameter = lineBuffer.Split(' ').Last();
                            double zHeight       = (lastParameter == "end") ? double.NaN : double.Parse(lastParameter, FrmMain.numberFormat);
                            if (lineBuffer.StartsWith("; layer 1, Z =", StringComparison.InvariantCulture))
                            {
                                firstLayerHeight = zHeight;
                            }

                            // Create a new one
                            layer   = new GCodeLayer(layer.Number + 1, zHeight);
                            segment = new GCodeSegment(lineBuffer, segment.Tool, segment);
                            layer.Segments.Add(segment);
                            isInterfacingSet = layer.Number < 2;
                        }
                        else if ((layer.Number == 0 && lineNumber > 2 && !lineBuffer.Contains("layerHeight")) ||
                                 lineBuffer.StartsWith("; tool", StringComparison.InvariantCulture) ||
                                 lineBuffer.StartsWith("; process", StringComparison.InvariantCulture))
                        {
                            // Keep first two comment lines but get rid of S3D process description and
                            // remove "; tool" as well as "; process" lines because they are completely useless
                            writeLine = false;

                            // Try to get the tool change parameters
                            if (lineBuffer.Contains("toolChangeRetractionDistance"))
                            {
                                double?value = line.GetFValue(',', true);
                                if (value.HasValue)
                                {
                                    toolChangeRetractionDistance = value.Value;
                                }
                            }
                            if (lineBuffer.Contains("toolChangeRetractionSpeed"))
                            {
                                double?value = line.GetFValue(',', true);
                                if (value.HasValue)
                                {
                                    toolChangeRetractionSpeed = value.Value;
                                }
                            }
                        }
                        else if (layer.Number > 0)
                        {
                            // T-codes are generated just before a new segment starts
                            string region = lineBuffer.Substring(lineBuffer.StartsWith("; feature", StringComparison.InvariantCulture) ? 9 : 1).Trim();
                            if (segment.Lines.Count == 0)
                            {
                                segment.Name = region;
                            }
                            else
                            {
                                segment = new GCodeSegment(region, segment.Tool, segment);
                                layer.Segments.Add(segment);
                            }
                        }
                    }
                    else
                    {
                        int?gCode = line.GetIValue('G');
                        if (gCode.HasValue)
                        {
                            // G0 / G1
                            if (gCode == 0 || gCode == 1)
                            {
                                double?xParam = line.GetFValue('X');
                                double?yParam = line.GetFValue('Y');
                                double?zParam = line.GetFValue('Z');
                                if (xParam.HasValue)
                                {
                                    lastPoint.X = xParam.Value;
                                }
                                if (yParam.HasValue)
                                {
                                    if (rotaryPrinting)
                                    {
                                        yParam = HandleRescale(firstLayerHeight, line, yParam);
                                    }
                                    lastPoint.Y = yParam.Value;
                                }
                                if (zParam.HasValue)
                                {
                                    lastPoint.Z = zParam.Value;
                                }

                                if (numExtrusions < 2)
                                {
                                    if (line.GetFValue('E').HasValue)
                                    {
                                        numExtrusions++;
                                        writeLine = false;
                                    }
                                }

                                double?fParam = line.GetFValue('F');
                                if (fParam.HasValue)
                                {
                                    feedrate = fParam.Value / 60.0;
                                }

                                if (!isInterfacingSet && segment.Tool != -1 && xParam.HasValue && yParam.HasValue)
                                {
                                    segment.IsInterfacing = GetClosestSegment(lastLayer, xParam.Value, yParam.Value)?.Tool != segment.Tool;
                                    isInterfacingSet      = true;
                                }
                            }
                            // G10
                            else if (gCode == 10)
                            {
                                int?   pParam = line.GetIValue('P');
                                double?sParam = line.GetFValue('S');
                                if (pParam.HasValue && pParam.Value > 0 && pParam.Value <= settings.Tools.Length && sParam.HasValue)
                                {
                                    // G10 P... S...
                                    settings.Tools[pParam.Value - 1].ActiveTemperature = (decimal)sParam.Value;
                                }
                            }
                            else if (gCode == 28)
                            {
                                lastPoint = homingPosition.Clone();
                            }
                        }
                        else
                        {
                            int?mCode = line.GetIValue('M');
                            if (mCode.HasValue)
                            {
                                // M104
                                if (mCode == 104)
                                {
                                    double?sParam = line.GetFValue('S');
                                    int?   tParam = line.GetIValue('T');
                                    if (sParam.HasValue && tParam.HasValue && tParam.Value > 0 && tParam.Value <= settings.Tools.Length)
                                    {
                                        ToolSettings toolSettings = settings.Tools[tParam.Value - 1];
                                        if (toolSettings.Type == ToolType.Nozzle)
                                        {
                                            if (toolSettings.ActiveTemperature <= 0m)
                                            {
                                                toolSettings.ActiveTemperature = (decimal)sParam.Value;
                                                segment.AddLine($"G10 P{tParam} R{toolSettings.StandbyTemperature.ToString(FrmMain.numberFormat)} S{toolSettings.ActiveTemperature.ToString(FrmMain.numberFormat)}");
                                            }
                                            else
                                            {
                                                segment.AddLine($"G10 P{tParam} S{sParam.Value.ToString(FrmMain.numberFormat)}");
                                            }
                                        }
                                        writeLine = false;
                                    }
                                }
                            }
                            else
                            {
                                // T-Code
                                int?tCode = line.GetIValue('T');
                                if (tCode.HasValue)
                                {
                                    if (tCode > 0 && tCode <= settings.Tools.Length)
                                    {
                                        usedTools.Add(tCode.Value);
                                        if (settings.Tools[tCode.Value - 1].Type == ToolType.Nozzle)
                                        {
                                            // Keep track of tools in use. Tool change sequences are inserted by the post-processor
                                            if (segment.Lines.Count <= 1)
                                            {
                                                segment.Tool = tCode.Value;
                                            }
                                            else
                                            {
                                                segment = new GCodeSegment(segment.Name, tCode.Value, segment);
                                                layer.Segments.Add(segment);
                                            }
                                            writeLine = false;
                                        }
                                        else
                                        {
                                            // Make sure we don't print with inproperly configured tools...
                                            throw new ProcessorException($"Tool {tCode} is not configured as a nozzle (see line {lineNumber})");
                                        }
                                    }
                                    else if (segment.Lines.Count == 0)
                                    {
                                        segment.Tool = -1;
                                    }
                                    else
                                    {
                                        segment = new GCodeSegment(segment.Name, -1, segment);
                                        layer.Segments.Add(segment);
                                    }
                                }
                            }
                        }
                    }

                    // Add this line unless it was handled before
                    if (writeLine)
                    {
                        line.Feedrate = feedrate;
                        segment.AddLine(line);
                    }
                    lineBuffer = await reader.ReadLineAsync();

                    lineNumber++;

                    // Report progress to the UI
                    progress.Report((int)input.Position);
                } while (lineBuffer != null);

                layers.Add(layer);
                RemoveHeatingOfUnusedTools(usedTools);
                if (rotaryPrinting)
                {
                    RotaryPrintingFixes();
                }
            }
            else if (lineBuffer.Contains("Diabase"))
            {
                throw new ProcessorException("File has been already processed");
            }
            else
            {
                throw new ProcessorException("File was not generated by Simplify3D");
            }
        }
        private void InsertPreheatingSequences(ref int iteration)
        {
            // Check if at least one tool needs preheating or quit otherwise
            if (!settings.Tools.Any(tool => tool.PreheatTime > 0.0m))
            {
                iteration += layers.Count * 2 - 1;
                progress.Report(iteration);
                return;
            }
            var preheatCounters  = new Dictionary <int, double>(); // Tool number vs. Elapsed time
            var position         = new Coordinate();
            var previousPosition = position.Clone();

            // First calculate duration of each line
            for (var layerNumber = 0; layerNumber < layers.Count - 1; layerNumber++)
            {
                var layer = layers[layerNumber];
                foreach (var segment in layer.Segments)
                {
                    foreach (var line in segment.Lines)
                    {
                        int?gCode = line.GetIValue('G');

                        // G0 / G1
                        if (gCode == 0 || gCode == 1)
                        {
                            var xParam = line.GetFValue('X');
                            var yParam = line.GetFValue('Y');
                            var zParam = line.GetFValue('Z');
                            var eParam = line.GetFValue('E');
                            if (xParam.HasValue)
                            {
                                position.X = xParam.Value;
                            }
                            if (yParam.HasValue)
                            {
                                position.Y = yParam.Value;
                            }
                            if (zParam.HasValue)
                            {
                                position.Z = zParam.Value;
                            }

                            line.Distance =
                                Math.Sqrt(
                                    Math.Pow(position.X - previousPosition.X, 2) +
                                    Math.Pow(position.Y - previousPosition.Y, 2) +
                                    Math.Pow(position.Z - previousPosition.Z, 2) +
                                    (eParam.HasValue ? Math.Pow(eParam.Value, 2) : 0)
                                    );
                            if (line.Feedrate > 0.0)
                            {
                                var rule     = GetRule(segment.Tool, layerNumber, segment);
                                var feedrate = line.Feedrate * (rule == null ? 1 : (rule.SpeedFactor / 100.0));
                                // TODO: Take into account accelerations here?
                                line.Duration = line.Distance / feedrate;
                            }

                            previousPosition.AssignFrom(position);
                        }
                        else if (gCode == 4)
                        {
                            var sParam = line.GetFValue('S');
                            if (sParam.HasValue)
                            {
                                line.Duration = sParam.Value;
                            }
                            else
                            {
                                var pParam = line.GetIValue('P');
                                if (pParam.HasValue)
                                {
                                    line.Duration = pParam.Value / 1000.0;
                                }
                            }
                        }
                        else if (gCode == 28)
                        {
                            previousPosition.AssignFrom(homingPosition);
                            position.AssignFrom(homingPosition);
                        }
                        else if (gCode == 32)
                        {
                            previousPosition.AssignFrom(afterProbingPosition);
                            position.AssignFrom(afterProbingPosition);
                        }
                    }
                }
                progress.Report(iteration++);
            }

            // Now go through the file backwards and insert preheating commands
            for (var layerNumber = layers.Count - 1; layerNumber >= 1; layerNumber--)
            {
                var layer = layers[layerNumber];

                // Start at last segment in layer
                for (var segmentNumber = layer.Segments.Count - 1; segmentNumber >= 0; segmentNumber--)
                {
                    var segment = layer.Segments[segmentNumber];

                    // Start at last line in layer
                    for (var lineNumber = segment.Lines.Count - 1; lineNumber >= 0; lineNumber--)
                    {
                        var line = segment.Lines[lineNumber];

                        // We found a tool change
                        if (line.Content.EndsWith(ToolChangeMarker, StringComparison.InvariantCulture))
                        {
                            // Take into account tool change times
                            var tool = settings.Tools[segment.Tool - 1];

                            // See if we need to use preheating for this tool
                            if (tool.PreheatTime > 0.0m)
                            {
                                // Reset possibly existing preheating time to just the tool change time
                                // In case we were already waiting we will have to wait even longer.
                                preheatCounters[segment.Tool] = (settings.Tools[segment.Tool - 1].AutoClean) ? ToolChangeDurationWithCleaning : ToolChangeDuration;
                            }
                        }

                        // We only care for other commands if we are taking time
                        if (preheatCounters.Count > 0)
                        {
                            var timeSpent = 0.0;
                            if (line.Duration.HasValue)
                            {
                                timeSpent = line.Duration.Value;
                            }
                            int?gCode = line.GetIValue('G');
                            if (gCode == 10)
                            {
                                var pParam = line.GetIValue('P');
                                var rParam = line.GetIValue('R');
                                if (pParam.HasValue && rParam.HasValue && pParam > 0 && pParam <= settings.Tools.Length)
                                {
                                    if (preheatCounters.ContainsKey(pParam.Value))
                                    {
                                        // Remove this line again if we are still preheating
                                        segment.Lines.RemoveAt(lineNumber);
                                    }
                                }
                            }

                            // Check if any of the tools we want to preheat has had enough time to do so yet
                            foreach (var toolNumber in preheatCounters.Keys.ToList())
                            {
                                var tool           = settings.Tools[toolNumber - 1];
                                var totalTimeSpent = preheatCounters[toolNumber] + timeSpent;
                                if (totalTimeSpent > (double)tool.PreheatTime)
                                {
                                    // We've been doing enough stuff to generate a good G10 code
                                    segment.Lines.Insert(lineNumber, new GCodeLine($"G10 P{toolNumber} R{tool.ActiveTemperature.ToString(FrmMain.numberFormat)}"));
                                    preheatCounters.Remove(toolNumber);
                                }
                                else
                                {
                                    // Need to do some more...
                                    preheatCounters[toolNumber] = totalTimeSpent;
                                }
                            }
                        }
                    }
                }
                progress.Report(iteration++);
            }

            // Override first generated G10 codes if we could not preheat in time
            if (preheatCounters.Count > 0 && layers.Count > 0 && layers[0].Segments.Count > 0)
            {
                foreach (GCodeLine line in layers[0].Segments[0].Lines)
                {
                    int?gCode = line.GetIValue('G');
                    if (gCode == 10)
                    {
                        int?pParam = line.GetIValue('P');
                        if (pParam.HasValue && preheatCounters.ContainsKey(pParam.Value))
                        {
                            ToolSettings tool       = settings.Tools[pParam.Value - 1];
                            var          activeTemp = tool.ActiveTemperature.ToString(FrmMain.numberFormat);
                            line.Content = $"G10 P{pParam} R{activeTemp} S{activeTemp}";
                            preheatCounters.Remove(pParam.Value);
                        }
                    }
                }
            }
        }