private line parse_line(sub_string l)
        {
            Debug.Assert(syntaxes_.Count > 0);

            foreach (var si in syntaxes_)
            {
                line result = null;
                if (si.relative_syntax_)
                {
                    result = parse_relative_line(l, si);
                }
                else
                {
                    result = parse_line_with_syntax(l, si);
                }

                if (result != null)
                {
                    return(result);
                }
            }

            // in this case, we can't parse the line at all - use default
            return(new line(l, syntax_info.line_contains_msg_only_));
        }
        // returns null if it can't parse
        private line parse_line_with_syntax(sub_string l, syntax_info si)
        {
            if (si.relative_syntax_)
            {
                return(parse_relative_line(l, si));
            }

            try {
                string sub         = l.msg;
                bool   normal_line = parse_time(sub, si.idx_in_line_[(int)info_type.time]) && parse_date(sub, si.idx_in_line_[(int)info_type.date]);
                if (si.idx_in_line_[(int)info_type.time].Item1 < 0 && si.idx_in_line_[(int)info_type.date].Item1 < 0)
                {
                    // in this case, we don't have time & date - see that the level matches
                    // note: we can't rely on level too much, since the user might have additional levels that our defaults - so we could get false negatives
                    normal_line = parse_level(sub, si.idx_in_line_[(int)info_type.level]);
                }

                return(normal_line ? new line(l, si.idx_in_line_) : null);
            } catch (Exception e) {
                logger.Error("invalid line: " + l + " : " + e.Message);
                //return new line(pos_in_log, l, line_contains_msg_only_);
                return(null);
            }
        }
        private line parse_relative_line(sub_string l, syntax_info si)
        {
            List <Tuple <int, int> > indexes = new List <Tuple <int, int> >();

            for (int i = 0; i < (int)info_type.max; ++i)
            {
                indexes.Add(new Tuple <int, int>(-1, -1));
            }

            string sub           = l.msg;
            int    cur_idx       = 0;
            int    correct_count = 0;

            foreach (var rel in si.relative_idx_in_line_)
            {
                if (cur_idx < 0)
                {
                    break;
                }
                var index = parse_relative_part(sub, rel, ref cur_idx);
                if (index == null)
                {
                    return(null);
                }
                if (index.Item1 >= 0)
                {
                    indexes[(int)rel.type] = index;
                    ++correct_count;
                }
            }

            // if we could parse time or date, we consider it an OK line
            bool normal_line = correct_count == si.relative_idx_in_line_.Count;

            return(normal_line ? new line(l, indexes.ToArray()) : null);
        }
        private line parse_line(sub_string l) {
            Debug.Assert(syntaxes_.Count > 0);

            foreach (var si in syntaxes_) {
                line result = null;
                if (si.relative_syntax_)
                    result = parse_relative_line(l, si);
                else
                    result = parse_line_with_syntax(l, si);

                if (result != null)
                    return result;
            }

            // in this case, we can't parse the line at all - use default
            return new line(l, syntax_info.line_contains_msg_only_);
        }
        // returns null if it can't parse
        private line parse_line_with_syntax(sub_string l, syntax_info si) {
            if (si.relative_syntax_)
                return parse_relative_line(l, si);

            try {
                string sub = l.msg;
                bool normal_line = parse_time(sub, si.idx_in_line_[(int) info_type.time]) && parse_date(sub, si.idx_in_line_[(int) info_type.date]);
                if (si.idx_in_line_[(int) info_type.time].Item1 < 0 && si.idx_in_line_[(int) info_type.date].Item1 < 0)
                    // in this case, we don't have time & date - see that the level matches
                    // note: we can't rely on level too much, since the user might have additional levels that our defaults - so we could get false negatives
                    normal_line = parse_level(sub, si.idx_in_line_[(int) info_type.level]);

                return normal_line ? new line(l, si.idx_in_line_) : null;
            } catch(Exception e) {
                logger.Error("invalid line: " + l + " : " + e.Message);
                //return new line(pos_in_log, l, line_contains_msg_only_);
                return null;
            }
        }
        private line parse_relative_line(sub_string l, syntax_info si) {
            List< Tuple<int,int> > indexes = new List<Tuple<int, int>>();
            for ( int i = 0; i < (int)info_type.max; ++i)
                indexes.Add(new Tuple<int,int>(-1,-1));

            string sub = l.msg;
            int cur_idx = 0;
            int correct_count = 0;
            foreach (var rel in si.relative_idx_in_line_) {
                if (cur_idx < 0)
                    break;
                var index = parse_relative_part(sub, rel, ref cur_idx);
                if (index == null)
                    return null;
                if (index.Item1 >= 0 && index.Item2 >= 0) {
                    indexes[(int) rel.type] = index;
                    ++correct_count;
                }
            }

            // if we could parse time or date, we consider it an OK line
            bool normal_line = correct_count == si.relative_idx_in_line_.Count;
            return normal_line ? new line(l, indexes.ToArray()) : null ;
        }
        public override void read_to_end()
        {
            ulong old_len = reader_.full_len;

            reader_.compute_full_length();
            ulong new_len = reader_.full_len;

            // when reader's position is zero -> it's either the first time, or file was re-rewritten
            if (old_len > new_len || reader_.pos == 0)
            {
                // file got re-written
                force_reload();
            }

            bool fully_read = old_len == new_len && reader_.is_up_to_date();

            if (!reader_.has_more_cached_text())
            {
                lock (this) {
                    up_to_date_ = fully_read;
                    if (up_to_date_)
                    {
                        // at this point, we're sure we read everything
                        was_last_line_incomplete_ = DateTime.MinValue;
                    }
                }
                return;
            }

            lock (this)
                up_to_date_ = false;

            string text = reader_.read_next_text();
            int    added_line_count = 0;
            bool   was_last_line_incomplete = false, is_last_line_incomplete = false;
            int    old_line_count = string_.line_count;

            string_.add_lines(text, ref added_line_count, ref was_last_line_incomplete, ref is_last_line_incomplete);

            if (added_line_count < 1)
            {
                return;
            }

            bool needs_reparse_last_line;

            lock (this)
                needs_reparse_last_line = lines_.Count > 0 && was_last_line_incomplete;
            int         start_idx         = old_line_count - (was_last_line_incomplete ? 1 : 0);
            int         end_idx           = string_.line_count;
            List <line> now               = new List <line>(end_idx - start_idx);
            int         merged_line_count = 0;

            for (int i = start_idx; i < end_idx; ++i)
            {
                var  cur_line          = new sub_string(string_, i - merged_line_count);
                bool is_from_prev_line = false;
                if (if_line_starts_with_tab_assume_from_prev_line)
                {
                    if (cur_line.msg.StartsWith("\t"))
                    {
                        is_from_prev_line = true;
                    }
                }
                if (i == 0)
                {
                    is_from_prev_line = false; // there's no previous line
                }
                if (!is_from_prev_line)
                {
                    now.Add(parse_line(cur_line));
                }
                else
                {
                    string_.merge_line_into_previous_line(i - merged_line_count);
                    ++merged_line_count;
                }
            }

            lock (this) {
                if (needs_reparse_last_line)
                {
                    // we re-parse the last line (which was previously incomplete)
                    logger.Debug("[line] reparsed line " + (old_line_count - 1));
                    lines_.RemoveAt(lines_.Count - 1);
                }

                int old_count = lines_.Count;
                lines_.AddRange(now);
                // in order to adjust time, we have to have at least one syntax in which we find it
                bool can_find_time = syntaxes_.FirstOrDefault(x => x.can_find(info_type.time)) != null;
                if (can_find_time)
                {
                    for (int idx = old_count; idx < lines_.Count; ++idx)
                    {
                        adjust_line_time(idx);
                    }
                }
                was_last_line_incomplete_ = was_last_line_incomplete ? DateTime.Now : DateTime.MinValue;
            }
            //Debug.Assert( lines_.Count == string_.line_count);

            update_log_lines_capacity();
        }
        public override void read_to_end() {
            ulong old_len = reader_.full_len;
            reader_.compute_full_length();
            ulong new_len = reader_.full_len;
            // when reader's position is zero -> it's either the first time, or file was re-rewritten
            if (old_len > new_len || reader_.pos == 0) 
                // file got re-written
                force_reload();

            bool fully_read = old_len == new_len && reader_.is_up_to_date();

            if ( !reader_.has_more_cached_text()) {
                lock (this) {
                    up_to_date_ = fully_read;
                    if ( up_to_date_)
                        // at this point, we're sure we read everything
                        was_last_line_incomplete_ = DateTime.MinValue;
                }
                return;
            }
            
            lock (this)
                up_to_date_ = false;

            string text = reader_.read_next_text();
            int added_line_count = 0;
            bool was_last_line_incomplete = false, is_last_line_incomplete = false;
            int old_line_count = string_.line_count;
            string_.add_lines(text, ref added_line_count, ref was_last_line_incomplete, ref is_last_line_incomplete);

            if (added_line_count < 1)
                return;

            bool needs_reparse_last_line;
            lock (this)
                needs_reparse_last_line = lines_.Count > 0 && was_last_line_incomplete ;
            int start_idx = old_line_count - (was_last_line_incomplete ? 1 : 0);
            int end_idx = string_.line_count;
            List<line> now = new List<line>(end_idx - start_idx);
            int merged_line_count = 0;
            for (int i = start_idx; i < end_idx; ++i) {
                var cur_line = new sub_string(string_, i - merged_line_count);
                bool is_from_prev_line = false;
                if ( if_line_starts_with_tab_assume_from_prev_line)
                    if (cur_line.msg.StartsWith("\t"))
                        is_from_prev_line = true;
                if (i == 0)
                    is_from_prev_line = false; // there's no previous line

                if (!is_from_prev_line)
                    now.Add(parse_line(cur_line));
                else {
                    string_.merge_line_into_previous_line(i - merged_line_count);
                    ++merged_line_count;
                }
            }

            lock (this) {
                if (needs_reparse_last_line) {
                    // we re-parse the last line (which was previously incomplete)
                    logger.Debug("[line] reparsed line " + (old_line_count-1) );
                    lines_.RemoveAt( lines_.Count - 1);
                }

                int old_count = lines_.Count;
                lines_.AddRange(now);
                // in order to adjust time, we have to have at least one syntax in which we find it
                bool can_find_time = syntaxes_.FirstOrDefault(x => x.can_find(info_type.time)) != null;
                if ( can_find_time)
                    for ( int idx = old_count; idx < lines_.Count; ++idx)
                        adjust_line_time(idx);
                was_last_line_incomplete_ = was_last_line_incomplete ? DateTime.Now : DateTime.MinValue;
            }
            Debug.Assert( lines_.Count == string_.line_count);

            update_log_lines_capacity();
        }